about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--Completion/Builtins/_zftp41
-rw-r--r--Doc/Zsh/mod_zftp.yo118
-rw-r--r--Doc/Zsh/zftpsys.yo123
-rw-r--r--Functions/Zftp/zfanon29
-rw-r--r--Functions/Zftp/zfautocheck12
-rw-r--r--Functions/Zftp/zfcd12
-rw-r--r--Functions/Zftp/zfcd_match12
-rw-r--r--Functions/Zftp/zfcget30
-rw-r--r--Functions/Zftp/zfdir30
-rw-r--r--Functions/Zftp/zffcache24
-rw-r--r--Functions/Zftp/zfgcp33
-rw-r--r--Functions/Zftp/zfget34
-rw-r--r--Functions/Zftp/zfget_match33
-rw-r--r--Functions/Zftp/zfgoto31
-rw-r--r--Functions/Zftp/zfinit22
-rw-r--r--Functions/Zftp/zfmark5
-rw-r--r--Functions/Zftp/zfopen30
-rw-r--r--Functions/Zftp/zfparams22
-rw-r--r--Functions/Zftp/zfpcp5
-rw-r--r--Functions/Zftp/zfrglob10
-rw-r--r--Functions/Zftp/zfsession71
-rw-r--r--Functions/Zftp/zfstat29
-rw-r--r--Functions/Zftp/zftp_chpwd26
-rw-r--r--Functions/Zftp/zftp_progress12
-rw-r--r--Functions/Zftp/zftransfer62
-rw-r--r--Functions/Zftp/zfuget50
-rw-r--r--Functions/Zftp/zfuput42
-rw-r--r--Src/Modules/zftp.c773
28 files changed, 1116 insertions, 605 deletions
diff --git a/Completion/Builtins/_zftp b/Completion/Builtins/_zftp
index 54cadc890..7aa1d94e8 100644
--- a/Completion/Builtins/_zftp
+++ b/Completion/Builtins/_zftp
@@ -1,19 +1,22 @@
 #compdef -p zf*
 
-# Don't try any more completion after this.
-_compskip=all
-
 # Completion for zftp builtin and zf* functions.  The functions
-# zfcd_match and zfget_match (used for old-style completion)
+# zfcd_match and zfget_match (also used for old-style completion)
 # need to be installed for remote file and directory completion to work.
 
+emulate -L zsh
+
+# Don't try any more completion after this.
+_compskip=all
+
 local subcom expl
 
 if [[ $words[1] = zftp ]]; then
   if [[ $CURRENT -eq 2 ]]; then
     _description expl sub-command
     compadd "$expl[@]" open params user login type ascii binary mode put \
-      putat get getat append appendat ls dir local remote mkdir rmdir
+      putat get getat append appendat ls dir local remote mkdir rmdir \
+      session rmsession
     return
   fi
   subcom=$words[2]
@@ -23,17 +26,13 @@ fi
 
 case $subcom in
   *(cd|ls|dir))
-    # complete remote directories; we could be smarter about hiding prefixes
+    # complete remote directories
     zfcd_match $PREFIX $SUFFIX
-    _description expl 'remote directory'
-    (( $#reply )) && compadd "$expl[@]" -S/ -q - $reply
     ;;
 
   *(get(|at)|gcp|delete|remote))
     # complete remote files
     zfget_match $PREFIX $SUFFIX
-    _description expl 'remote file'
-    (( $#reply )) && compadd "$expl[@]" -F fignore - $reply
     ;;
 
   *(put(|at)|pcp))
@@ -60,6 +59,28 @@ case $subcom in
     fi
     ;;
 
+  *session)
+    # complete sessions, excluding the current one.
+    _description expl 'another FTP session'
+    compadd "$expl[@]" - ${$(zftp session):#$ZFTP_SESSION}
+    ;;
+
+  *transfer)
+    # complete arguments like sess1:file1 sess2:file2
+    if [[ $PREFIX = *:* ]]; then
+      # complete file in the given session
+      local sess=${PREFIX%%:*} oldsess=$ZFTP_SESSION
+      compset -p $(( $#sess + 1 ))
+      [[ -n $sess ]] && zftp session $sess
+      zfget_match $PREFIX $SUFFIX
+      [[ -n $sess && -n $oldsess ]] && zftp session $oldsess
+    else
+      # note here we can complete the current session
+      _description expl 'FTP session'
+      compadd "$expl[@]" -S : - $(zftp session)
+    fi
+    ;;
+
   *)
     # dunno... try ordinary completion after all.
     _compskip=''
diff --git a/Doc/Zsh/mod_zftp.yo b/Doc/Zsh/mod_zftp.yo
index f5d165083..e5dc8ec2f 100644
--- a/Doc/Zsh/mod_zftp.yo
+++ b/Doc/Zsh/mod_zftp.yo
@@ -63,13 +63,20 @@ xitem(tt(params) [ var(host) [ var(user) [ var(password) \
 item(tt(params) tt(-))(
 Store the given parameters for a later tt(open) command with no
 arguments.  Only those given on the command line will be remembered.
-Any of the parameters may, however, be specified as a `tt(?)', which
-may need to be quoted to protect it from shell expansion:  in this case,
-the appropriate parameter will be read from stdin as with the
-tt(login) subcommand, including special handling of var(password).
-
 If no arguments are given, the parameters currently set are printed,
-although the password will appear as a line of stars.
+although the password will appear as a line of stars; the return value is
+one if no parameters were set, zero otherwise.
+
+Any of the parameters may be specified as a `tt(?)', which
+may need to be quoted to protect it from shell expansion.  In this case,
+the appropriate parameter will be read from stdin as with the
+tt(login) subcommand, including special handling of var(password).  If the
+`tt(?)' is followed by a string, that is used as the prompt for reading the
+parameter instead of the default message (any necessary punctuation and
+whitespace should be included at the end of the prompt).  The first letter
+of the parameter (only) may be quoted with a `tt(\)'; hence an argument
+tt("\\$word") guarantees that the string from the shell parameter tt($word)
+will be treated literally, whether or not it begins with a `tt(?)'.
 
 If instead a single `tt(-)' is given, the existing parameters, if any,
 are deleted.  In that case, calling tt(open) with no arguments will
@@ -80,20 +87,10 @@ will be deleted if the tt(zftp) module is unloaded.
 
 For example,
 
-example(zftp params ftp.elsewhere.xx juser '?')
+example(zftp params ftp.elsewhere.xx juser '?Password for juser: ')
 
 will store the host tt(ftp.elsewhere.xx) and the user tt(juser) and
-then prompt the user for the corresponding password.
-
-This command may also be used to set up a transfer which then takes
-place completely in the background, freeing tt(zftp) for concurrent
-foreground use.  For example,
-
-example(zftp params ftp.soreeyes.ca bubble squeak
-(zftp open; zftp get foo >bar; zftp close) &)
-
---- here, the connection is restricted to a background subshell and
-you are free to open a simultaneous connection in the foreground.
+then prompt the user for the corresponding password with the given prompt.
 )
 item(tt(test))(
 Test the connection; if the server has reported
@@ -105,16 +102,11 @@ mechanism, or error messages if the connection closes.  There is no
 network overhead for this test.
 
 The test is only supported on systems with either the tt(select(2)) or
-tt(poll(2)) system calls; otherwise the message tt(not
-supported on this system) is printed instead.
+tt(poll(2)) system calls; otherwise the message `tt(not
+supported on this system)' is printed instead.
 
-It is useful to put the code
-
-example([[ -n $ZFTP_HOST ]] && zftp test)
-
-into the shell function tt(precmd) for testing the connection before
-every prompt.  However, tt(zftp) will call tt(test) at the start of any
-other subcommand when a connection is open.
+The tt(test) subcommand will automatically be called at the start of any
+other subcommand for the current session when a connection is open.
 )
 item(tt(cd) var(directory))(
 Change the remote directory to var(directory).  Also alters the shell
@@ -138,7 +130,7 @@ Otherwise, up to vagaries of the server implementation, behaves
 similar to tt(dir).
 )
 item(tt(type) [ var(type) ])(
-Change the type for transfer to var(type), or print the current type
+Change the type for the transfer to var(type), or print the current type
 if var(type) is absent.  The allowed values are `tt(A)' (ASCII),
 `tt(I)' (Image, i.e. binary), or `tt(B)' (a synonym for `tt(I)').
 
@@ -230,7 +222,32 @@ xitem(tt(close))
 item(tt(quit))(
 Close the current data connection.  This unsets the shell parameters
 tt(ZFTP_HOST), tt(ZFTP_IP), tt(ZFTP_SYSTEM), tt(ZFTP_USER),
-tt(ZFTP_ACCOUNT) and tt(ZFTP_PWD).
+tt(ZFTP_ACCOUNT), tt(ZFTP_PWD), tt(ZFTP_TYPE) and tt(ZFTP_MODE).
+)
+item(tt(session) [ var(sessname) ])(
+Allows multiple FTP sessions to be used at once.  The name of the session
+is an arbitrary string of characters; the default session is called
+`tt(default)'.  If this command is called without an argument, it will list
+all the current sessions; with an argument, it will either switch to the
+existing session called var(sessname), or create a new session of that name.
+
+Each session remembers the status of the connection, the set of
+connection-specific shell parameters (the same set as are unset when a
+connection closes, as given in the description of tt(close)), and any user
+parameters specified with the tt(params) subcommand.  Changing to a
+previous session restores those values; changing to a new session
+initialises them in the same way as if tt(zftp) had just been loaded.  The
+name of the current session is given by the parameter tt(ZFTP_SESSION).
+)
+item(tt(rmsession) [ var(sessname) ])(
+Delete a session; if a name is not given, the current session is deleted.
+If the current session is deleted, the earliest existing session becomes
+the new current session, otherwise the current session is not changed.
+If the session being deleted is the only one, a new session called
+`tt(default)' is created and becomes the current session; note that this is
+a new session even if the session being deleted is also called
+`tt(default)'. It is recommended that sessions not be deleted while
+background commands which use tt(zftp) are still active.
 )
 enditem()
 
@@ -276,8 +293,8 @@ vindex(ZFTP_USER)
 item(tt(ZFTP_USER))(
 Readonly.  The username currently logged in, if any.
 )
-vindex(ZFTP_ACCT)
-item(tt(ZFTP_ACCT))(
+vindex(ZFTP_ACCOUNT)
+item(tt(ZFTP_ACCOUNT))(
 Readonly.  The account name of the current user, if any.  Most servers
 do not require an account name.
 )
@@ -288,12 +305,19 @@ Readonly.  The current directory on the server.
 vindex(ZFTP_CODE)
 item(tt(ZFTP_CODE))(
 Readonly.  The three digit code of the last FTP reply from the server
-as a string.  This can still be read after the connection is closed.
+as a string.  This can still be read after the connection is closed, and
+is not changed when the current session changes.
 )
 vindex(ZFTP_REPLY)
 item(tt(ZFTP_REPLY))(
 Readonly.  The last line of the last reply sent by the server.  This
-can still be read after the connection is closed.
+can still be read after the connection is closed, and is not changed when
+the current session changes.
+)
+vindex(ZFTP_SESSION)
+item(tt(ZFTP_SESSION))(
+Readonly.  The name of the current FTP session; see the description of the
+tt(session) subcommand.
 )
 vindex(ZFTP_PREFS)
 item(tt(ZFTP_PREFS))(
@@ -430,21 +454,25 @@ Sometimes the progress meter may cause disruption.  It is up to the
 user to decide whether the function should be defined and to use
 tt(unfunction) when necessary.
 )
+enditem()
 
 subsect(Problems)
 cindex(zftp, problems)
 
-With the exception noted for the tt(params) subcommand, a connection
-may not be opened in the left hand side of a pipe as this occurs in a
-subshell and the file information is not updated in the main shell.
-In the case of type or mode changes or closing the connection in a
-subshell, the information is returned but variables are not updated
-until the next call to tt(zftp).  Other status changes in subshells
-will not be reflected by changes to the variables (but should
-be otherwise harmless).
+A connection may not be opened in the left hand side of a pipe as this
+occurs in a subshell and the file information is not updated in the main
+shell.  In the case of type or mode changes or closing the connection in a
+subshell, the information is returned but variables are not updated until
+the next call to tt(zftp).  Other status changes in subshells will not be
+reflected by changes to the variables (but should be otherwise harmless).
 
-On some operating systems, the control connection is not valid after a
-fork(), so that operations in subshells or on the left hand side of a
-pipeline are not possible.
+Deleting sessions while a tt(zftp) command is active in the background can
+have unexpected effects, even if it does not use the session being deleted.
+This is because all shell subprocesses share information on the state of
+all connections, and deleting a session changes the ordering of that
+information.
 
-enditem()
+On some operating systems, the control connection is not valid after a
+fork(), so that operations in subshells, on the left hand side of a
+pipeline, or in the background are not possible, as they should be.  This
+is presumably a bug in the operating system.
diff --git a/Doc/Zsh/zftpsys.yo b/Doc/Zsh/zftpsys.yo
index 3c8f90cdd..89d39b985 100644
--- a/Doc/Zsh/zftpsys.yo
+++ b/Doc/Zsh/zftpsys.yo
@@ -54,7 +54,7 @@ following code in your tt(.zshrc) will arrange for this; assume the
 functions are stored in the directory tt(~/myfns):
 
 example(fpath=(~/myfns $fpath)
-autoload zfinit
+autoload -U zfinit
 zfinit)
 
 Note that tt(zfinit) assumes you are using the tt(zmodload) method to
@@ -67,7 +67,10 @@ texinode(Zftp Functions)(Miscellaneous Features)(Installation)(Zftp Function Sys
 sect(Functions)
 
 The sequence of operations in performing a file transfer is essentially the
-same as that in a standard FTP client.
+same as that in a standard FTP client.  Note that, due to a quirk of the
+shell's tt(getopts) builtin, for those functions that handle options you
+must use `tt(-)tt(-)' rather than `tt(-)' to ensure the remaining arguments
+are treated literally (a single `tt(-)' is treated as an argument).
 
 subsect(Opening a connection)
 startitem()
@@ -77,16 +80,20 @@ Set or show the parameters for a future tt(zfopen) with no arguments.  If
 no arguments are given, the current parameters are displayed (the password
 will be shown as a line of asterisks).  If a host is given, and either the
 var(user) or var(password) is not, they will be prompted for; also, any
-parameter given as `tt(?)' will be prompted for.
-
-As tt(zfopen) calls tt(zfparams) to store the parameters, this usually need
-not be called directly.
+parameter given as `tt(?)' will be prompted for, and if the `tt(?)' is
+followed by a string, that will be used as the prompt.  As tt(zfopen) calls
+tt(zfparams) to store the parameters, this usually need not be called
+directly.
+
+A single argument `tt(-)' will delete the stored parameters.  This will
+also cause the memory of the last directory (and so on) on the other host
+to be deleted.
 )
 findex(zfopen)
 item(tt(zfopen [ -1 ] [ var(host) [ var(user) [ var(password) [ var(account) ] ] ] ]))(
 If var(host) is present, open a connection to that host under username
 var(user) with password var(password) (and, on the rare occasions when it
-is necessary account var(account)).  If a necessary parameter is missing or
+is necessary, account var(account)).  If a necessary parameter is missing or
 given as `tt(?)' it will be prompted for.  If var(host) is not present, use
 a previously stored set of parameters.
 
@@ -106,12 +113,12 @@ var(host), then change directory to var(path) (which must be a directory,
 not a file).  The `tt(ftp://)' can be omitted; the trailing `tt(/)' is enough
 to trigger recognition of the var(path).  Note prefixes other than
 `tt(ftp:)' are not recognized, and that all characters after the first
-slash beyond tt(host) are significant in var(path).
+slash beyond var(host) are significant in var(path).
 )
 findex(zfanon)
 item(tt(zfanon [ -1 ] var(host)))(
 Open a connection var(host) for anonymous FTP.  The username used is
-tt(anonymous).  The password (which will be reported the first time) is
+`tt(anonymous)'.  The password (which will be reported the first time) is
 generated as var(user)tt(@)var(host); this is then stored in the shell
 parameter tt($EMAIL_ADDR) which can alternatively be set manually to a
 suitable string.
@@ -143,12 +150,12 @@ expected, the string which on the local host corresponds to `tt(~)' is
 converted back to a `tt(~)' before being passed to the remote machine.
 This is convenient because of the way expansion is performed on the command
 line before tt(zfcd) receives a string.  For example, suppose the command
-is `tt(zfcd ~/foo)'.  The shell will expand this to a full path as in
+is `tt(zfcd ~/foo)'.  The shell will expand this to a full path such as
 `tt(zfcd /home/user2/pws/foo)'.  At this stage, tt(zfcd) recognises the
-initial path as `tt(~)', and the directory sent to the remote host is
-tt(~/foo), so that the `tt(~)' will be expanded by the server to the
-correct remote host directory.  Other named directories of the form
-`tt(~name)' are not treated in this fashion.
+initial path as corresponding to `tt(~)' and will send the directory to
+the remote host as tt(~/foo), so that the `tt(~)' will be expanded by the
+server to the correct remote host directory.  Other named directories of
+the form `tt(~name)' are not treated in this fashion.
 )
 findex(zfhere)
 item(tt(zfhere))(
@@ -162,21 +169,23 @@ item(tt(zfdir [ -rfd ] [ - ] [ var(dir-options) ] [ var(dir) ]))(
 Produce a long directory listing.  The arguments var(dir-options) and
 var(dir) are passed directly to the server and their effect is
 implementation dependent, but specifying a particular remote directory
-var(dir) is usually possible.  The output is passed through pager.
+var(dir) is usually possible.  The output is passed through a pager
+given by the environment variable tt($PAGER) or defaulting to `tt(more)'.
 
 The directory is usually cached for re-use.  In fact, two caches are
 maintained.  One is for use when there is no var(dir-options) or var(dir),
 i.e. a full listing of the current remote directory; it is flushed
 when the current remote directory changes.  The other is
 kept for repeated use of tt(zfdir) with the same arguments; for example,
-repeated use of tt(zfdir /pub/gnu) will only require the directory to be
+repeated use of `tt(zfdir /pub/gnu)' will only require the directory to be
 retrieved on the first call.  Alternatively, this cache can be re-viewed with
 the tt(-r) option.  As relative directories will confuse
-tt(zfdir), the tt(-f) option can be used to force the cache to be flushed.
-Also, the option tt(-d) will delete both caches without showing a directory
-listing.
+tt(zfdir), the tt(-f) option can be used to force the cache to be flushed
+before the directory is listed.  The option tt(-d) will delete both
+caches without showing a directory listing; it will also delete the cache
+of file names in the current remote directory, if any.
 )
-findex(zfdir)
+findex(zfls)
 item(tt(zfls) [ var(ls-options) ] [ var(dir) ])(
 List files on the remote server.  With no arguments, this will produce a
 simple list of file names for the current remote directory.  Any arguments
@@ -311,6 +320,49 @@ Close the connection.
 )
 enditem()
 
+subsect(Session management)
+startitem()
+findex(zfsession)
+item(tt(zfsession) [ tt(-lvod) ] [ var(sessname) ])(
+Allows you to manage multiple FTP sessions at once.  By default,
+connections take place in a session called `tt(default)'; by giving the
+command `tt(zfsession) var(sessname)' you can change to a new or existing
+session with a name of your choice.  The new session remembers its own
+connection, as well as associated shell parameters, and also the host/user
+parameters set by tt(zfparams).  Hence you can have different sessions set
+up to connect to different hosts, each remembering the appropriate host,
+user and password.
+
+With no arguments, tt(zfsession) prints the name of the current session;
+with the option tt(-l) it lists all sessions which currently exist, and
+with the option tt(-v) it gives a verbose list showing the host and
+directory for each session, where the current session is marked with an
+asterisk.  With tt(-o), it will switch to the most recent previous session.
+
+With tt(-d), the given session (or else the current one) is removed;
+everything to do with it is completely forgotten.  If it was the only
+session, a new session called `tt(default)' is created and made current.
+It is safest not to delete sessions while background commands using
+tt(zftp) are active.
+)
+findex(zftransfer)
+item(tt(zftransfer) var(sess1)tt(:)var(file1) var(sess2)tt(:)var(file2))(
+Transfer files between two sessions; no local copy is made.  The file
+is read from the session var(sess1) as var(file1) and written to session
+var(sess1) as file var(file2); var(file1) and var(file2) may be relative to
+the current directories of the sesssion.  Either var(sess1) or var(sess2)
+may be omitted (though the colon should be retained if there is a
+possibility of a colon appearing in the file name) and defaults to the
+current session; var(file2) may be omitted or may end with a slash, in
+which case the basename of var(file1) will be added.  The sessions
+var(sess1) and var(sess2) must be distinct.
+
+The operation is performed using pipes, so it is required that the
+connections still be valid in a subshell, which is not the case under some
+operating systems.
+)
+enditem()
+
 subsect(Bookmarks)
 The two functions tt(zfmark) and tt(zfgoto) allow you to `bookmark' the
 present location (host, user and directory) of the current FTP connection
@@ -329,16 +381,18 @@ closed; it is an error if there is none.  Any existing bookmark
 under the same name will be silently replaced.
 
 If not given an argument, list the existing bookmarks and the points to
-which they refer in the form var(user)tt(@)var(host)tt(:)var(directory).
+which they refer in the form var(user)tt(@)var(host)tt(:)var(directory);
+this is the format in which they are stored, and the file may be edited
+directly.
 )
 findex(zfgoto)
 item(tt(zfgoto [ -n ] )var(bookmark))(
 Return to the location given by var(bookmark), as previously set by
 tt(zfmark).  If the location has user `tt(ftp)' or `tt(anonymous)', open
 the connection with tt(zfanon), so that no password is required.  If the
-user and host parameters match those currently stored, those will be used,
-and again no password is required.  Otherwise a password will be prompted
-for.
+user and host parameters match those stored for the current session, if
+any, those will be used, and again no password is required.  Otherwise a
+password will be prompted for.
 
 With the option tt(-n), the bookmark is taken to be a nickname stored by
 the tt(ncftp) program in its bookmark file, which is assumed to be
@@ -434,6 +488,11 @@ error, as standard output may be a file being received.  The form of the
 progess meter, or whether it is used at all, can be configured without
 altering the function, as described in the next section.
 )
+findex(zffcache)
+item(tt(zffcache))(
+This is used to implement caching of files in the current directory for
+each session separately.  It is used by tt(zfget_match) and tt(zfrglob).
+)
 enditem()
 
 texinode(Miscellaneous Features)()(Zftp Functions)(Zftp Function System)
@@ -446,7 +505,10 @@ pindex(zfconfig)
 The tt(zfinit) function defines an associative array tt(zfconfig).
 Elements of this may subsequently be set to change the behaviour of the
 tt(zftp) functions using standard syntax (for example,
-`tt(zfconfig[progress]=percent)'.  The following keys are understood.
+`tt(zfconfig[progress]=percent)'.  tt(zfconfig) may also contain
+various other values used by the function system, so it should not be used
+as the target of an assignment for a complete array.  The following keys
+are understood.
 
 startitem()
 item(tt(progress))(
@@ -525,7 +587,8 @@ never close the connection automatically.
 Information about the previous connection is given by the tt(zfstat)
 function.  So, for example, if that reports:
 
-example(Not connected.
+example(Session:        default
+Not connected.
 Last session:   ftp.bar.com:/pub/textfiles)
 
 then the command tt(zfget file.txt) will attempt to reopen a connection to
@@ -533,10 +596,14 @@ tt(ftp.bar.com), retrieve the file tt(/pub/textfiles/file.txt), and
 immediately close the connection again.  On the other hand, tt(zfcd ..)
 will open the connection in the directory tt(/pub) and leave it open.
 
+Note that all the above is local to each session; if you return to a
+previous session, the connection for that session is the one which will be
+reopened.
+
 subsect(Completion)
 
-Completion of local and remote files, directories and bookmarks is
-supported.  The older, tt(compctl)-style completion is defined when
+Completion of local and remote files, directories, sessions and bookmarks
+is supported.  The older, tt(compctl)-style completion is defined when
 tt(zfinit) is called; support for the new widget-based completion system is
 provided in the function tt(Completion/Builtins/_zftp), which should be
 installed with the other functions of the completion system and hence
diff --git a/Functions/Zftp/zfanon b/Functions/Zftp/zfanon
index 9624d48d9..f5754dda6 100644
--- a/Functions/Zftp/zfanon
+++ b/Functions/Zftp/zfanon
@@ -2,31 +2,19 @@
 
 emulate -L zsh
 
-local opt optlist once dir
+local opt opt_1 dir
 
-while [[ $1 = -* ]]; do
-  if [[ $1 = - || $1 = -- ]]; then
-    shift;
-    break;
-  fi
-  optlist=${1#-}
-  for (( i = 1; i <= $#optlist; i++)); do
-    opt=$optlist[$i]
-    case $optlist[$i] in
-      1) once=1
-	 ;;
-      *) print option $opt not recognised >&2
-	 ;;
-    esac
-  done
-  shift
+while getopts :1 opt; do
+  [[ $opt = "?" ]] && print "zfanon: bad option: -$OPTARG" >&2 && return 1
+  eval "opt_$opt=1"
 done
+(( OPTIND > 1 )) && shift $(( OPTIND - 1 ))
 
 if [[ -z $EMAIL_ADDR ]]; then
   # Exercise in futility.  There's a poem by Wallace Stevens
   # called something like `N ways of looking at a blackbird',
   # where N is somewhere around 0x14 to 0x18.  Now zftp is
-  # ashamed to prsent `N ways of looking at a hostname'.
+  # ashamed to present `N ways of looking at a hostname'.
   local domain host
   # First, maybe we've already got it.  Zen-like.
   if [[ $HOST = *.* ]]; then
@@ -38,7 +26,8 @@ if [[ -z $EMAIL_ADDR ]]; then
     [[ -n $domain ]] && host=$HOST.$domain
   fi
   # Next, maybe we've got nlsookup.  May not work on LINUX.
-  [[ -z $host ]] && host=$(nslookup $HOST | awk '/Name:/ { print $2 }')
+  [[ -z $host ]] && host=$(nslookup $HOST 2>/dev/null |
+    awk '/Name:/ { print $2 }')
   if [[ -z $host ]]; then
     # we're running out of ideas, but this should work.
     # after all, i wrote it...
@@ -67,7 +56,7 @@ if [[ $1 = */* ]]; then
   1=${1%%/*}
 fi
 
-if [[ $once = 1 ]]; then
+if [[ $opt_1 = 1 ]]; then
   zftp open $1 anonymous $EMAIL_ADDR || return 1
 else
   zftp params $1 anonymous $EMAIL_ADDR
diff --git a/Functions/Zftp/zfautocheck b/Functions/Zftp/zfautocheck
index d06beca4f..993e84ebf 100644
--- a/Functions/Zftp/zfautocheck
+++ b/Functions/Zftp/zfautocheck
@@ -11,9 +11,12 @@
 # command, which implies we are looking for something so should stay open
 # for it.
 
-# Remember the old session:  zflastsession will be overwritten by
+# Remember the old location:  will be overwritten by
 # a successful open.
-local lastsession=$zflastsession
+local lastloc=$zfconfig[lastloc_$ZFTP_SESSION]
+
+# Don't print out user messages when re-opening the connection.
+local ZFTP_VERBOSE=${ZFTP_VERBOSE//0}
 
 # Unset the delay counter from the progress meter in case there was an
 # abnormal exit.
@@ -30,8 +33,9 @@ fi
 
 if [[ $1 = *n* ]]; then
   return 0
-elif [[ -n $lastsession && $ZFTP_HOST = ${lastsession%%:*} ]]; then
-  zfcd ${lastsession#*:}
+elif [[ -n $lastloc && $ZFTP_HOST = ${lastloc%%:*} ]]; then
+  # don't print directory since we're just going back where we were.
+  zfcd ${lastloc#*:} >& /dev/null
 fi
 
 # }
diff --git a/Functions/Zftp/zfcd b/Functions/Zftp/zfcd
index b726d9f55..2ecc8b0f6 100644
--- a/Functions/Zftp/zfcd
+++ b/Functions/Zftp/zfcd
@@ -22,9 +22,9 @@
 emulate -L zsh
 
 if [[ $1 = /* ]]; then
-  zfautocheck -dn
+  zfautocheck -dn || return 1
 else
-  zfautocheck -d
+  zfautocheck -d || return 1
 fi
 
 if [[ $1 = $HOME || $1 = $HOME/* ]]; then
@@ -36,7 +36,7 @@ if (( $# == 0 )); then
   set -- '~'
 elif [[ $# -eq 1 && $1 = - ]]; then
   # Emulate `cd -' behaviour.
-  set -- $zflastdir
+  set -- $zfconfig[lastdir_$ZFTP_SESSION]
 elif [[ $# -eq 2 ]]; then
   # Emulate `cd old new' behaviour.
   # We have to find a character not in $1 or $2; ! is a good bet.
@@ -47,6 +47,8 @@ fi
 # if we want to keep it.
 local lastdir=$ZFTP_PWD
 
-zftp cd "$@"  &&  zflastdir=$lastdir
-print $zflastsession
+zftp cd "$@" && [[ $lastdir != $ZFTP_PWD ]] &&
+zfconfig[lastdir_$ZFTP_SESSION]=$lastdir
+
+print $zfconfig[lastloc_$ZFTP_SESSION]
 # }
diff --git a/Functions/Zftp/zfcd_match b/Functions/Zftp/zfcd_match
index 67e719888..e9d283c97 100644
--- a/Functions/Zftp/zfcd_match
+++ b/Functions/Zftp/zfcd_match
@@ -15,7 +15,7 @@ local tmpf=${TMPPREFIX}zfcm$$
 
 if [[ $ZFTP_SYSTEM = UNIX* ]]; then
   # hoo, aren't we lucky: this makes things so much easier
-  setopt localoptions rcexpandparam
+  setopt rcexpandparam
   local dir
   if [[ $1 = ?*/* ]]; then
     dir=${1%/*}
@@ -25,13 +25,15 @@ if [[ $ZFTP_SYSTEM = UNIX* ]]; then
   # If we're using -F, we get away with using a directory
   # to list, but not a glob.  Don't ask me why.
   # I hate having to rely on awk here.
-  zftp ls -F $dir >$tmpf
+  zftp ls -LF $dir >$tmpf
   reply=($(awk '/\/$/ { print substr($1, 0, length($1)-1) }' $tmpf))
   rm -f $tmpf
-  if [[ $dir = / ]]; then
-    reply=(${dir}$reply)
+  [[ -n $dir && $dir != */ ]] && dir="$dir/"
+  if [[ -n $WIDGET ]]; then
+    _description expl 'remote directory'
+    compadd -S/ -q -P "$dir" - $reply
   elif [[ -n $dir ]]; then
-    reply=($dir/$reply)
+    reply=(${dir}$reply)
   fi
 else
   # I simply don't know what to do here.
diff --git a/Functions/Zftp/zfcget b/Functions/Zftp/zfcget
index fd6accfed..f95f37704 100644
--- a/Functions/Zftp/zfcget
+++ b/Functions/Zftp/zfcget
@@ -12,35 +12,21 @@
 
 emulate -L zsh
 
-local loc rem stat=0 optlist opt nglob remlist locst remst
-local tmpfile=${TMPPREFIX}zfcget$$ rstat tsize time
+local loc rem stat=0 opt opt_G opt_t remlist locst remst
+local tmpfile=${TMPPREFIX}zfcget$$ rstat tsize
 
-while [[ $1 = -* ]]; do
-  if [[ $1 = - || $1 = -- ]]; then
-    shift;
-    break;
-  fi
-  optlist=${1#-}
-  for (( i = 1; i <= $#optlist; i++)); do
-    opt=$optlist[$i]
-    case $optlist[$i] in
-      G) nglob=1
-	 ;;
-      t) time=1
-	 ;;
-      *) print option $opt not recognised >&2
-	 ;;
-    esac
-  done
-  shift
+while getopts :Gt opt; do
+  [[ $opt = '?' ]] && print "zfcget: bad option: -$OPTARG" && return 1
+  eval "opt_$opt=1"
 done
+(( OPTIND > 1 )) && shift $(( OPTIND - 1 ))
 
 for remlist in $*; do
   # zfcd directory hack to put the front back to ~
   if [[ $remlist = $HOME || $remlist = $HOME/* ]]; then
     remlist="~${remlist#$HOME}"
   fi
-  if [[ $nglob != 1 ]]; then
+  if [[ $opt_G != 1 ]]; then
     zfrglob remlist
   fi
   if (( $#remlist )); then
@@ -73,7 +59,7 @@ for remlist in $*; do
 	  continue
 	else
 	  if zftp getat $rem $locst[1] >>$loc; then
-	    [[ $time = 1 ]] && zfrtime $loc $rem $remst[2]
+	    [[ $opt_t = 1 ]] && zfrtime $loc $rem $remst[2]
 	  else
 	    stat=1
 	  fi
diff --git a/Functions/Zftp/zfdir b/Functions/Zftp/zfdir
index 55befe000..deb5b9762 100644
--- a/Functions/Zftp/zfdir
+++ b/Functions/Zftp/zfdir
@@ -23,6 +23,8 @@ emulate -L zsh
 setopt extendedglob
 
 local file opt optlist redir i newargs force
+local curdir=$zfconfig[curdir_$ZFTP_SESSION]
+local otherdir=$zfconfig[otherdir_$ZFTP_SESSION]
 
 while [[ $1 = -* ]]; do
   if [[ $1 = - || $1 = -- ]]; then
@@ -40,9 +42,9 @@ while [[ $1 = -* ]]; do
 	 ;;
       f) force=1
 	 ;;
-      d) [[ -n $zfcurdir && -f $zfcurdir ]] && rm -f $zfcurdir
-	 [[ -n $zfotherdir && -f $zfotherdir ]] && rm -f $zfotherdir
-	 zftp_fcache=()
+      d) [[ -n $curdir && -f $curdir ]] && rm -f $curdir
+	 [[ -n $otherdir && -f $otherdir ]] && rm -f $otherdir
+	 zffcache -d
 	 return 0
 	 ;;
     esac
@@ -50,7 +52,7 @@ while [[ $1 = -* ]]; do
   shift
 done
 
-zfautocheck -d
+zfautocheck -d || return 1
 
 # directory hack, see zfcd
 for (( i = 1; i <= $#argv; i++ )); do
@@ -62,26 +64,32 @@ done
 if [[ $# -eq 0 ]]; then
   # Cache it in the current directory file.  This means that repeated
   # calls to zfdir with no arguments always use a cached file.
-  [[ -z $zfcurdir ]] && zfcurdir=${TMPPREFIX}zfcurdir$$
-  file=$zfcurdir
+  if [[ -z $curdir ]]; then
+    curdir=${TMPPREFIX}zfcurdir_${ZFTP_SESSION}_$$
+    zfconfig[curdir_$ZFTP_SESSION]=$curdir
+  fi
+  file=$curdir
 else
   # Last directly looked at was not the current one, or at least
   # had non-standard arguments.
-  [[ -z $zfotherdir ]] && zfotherdir=${TMPPREFIX}zfotherdir$$
-  file=$zfotherdir
+  if [[ -z $otherdir ]]; then
+    otherdir=${TMPPREFIX}zfotherdir_${ZFTP_SESSION}_$$
+    zfconfig[otherdir_$ZFTP_SESSION]=$otherdir
+  fi
+  file=$otherdir
   newargs="$*"
   if [[ -f $file && $redir != 1 && $force -ne 1 ]]; then
     # Don't use the cached file if the arguments changed.
-    [[ $newargs = $zfotherargs ]] || rm -f $file
+    [[ $newargs = $zfconfig[otherargs_$ZFTP_SESSION] ]] || rm -f $file
   fi
-  zfotherargs=$newargs
+  zfconfig[otherargs_$ZFTP_SESSION]=$newargs
 fi
 
 if [[ $force -eq 1 ]]; then
   rm -f $file
   # if it looks like current directory has changed, better invalidate
   # the filename cache, too.
-  (( $# == 0 )) && zftp_fcache=()
+  (( $# == 0 )) && zffcache -d
 fi
 
 if [[ -n $file && -f $file ]]; then
diff --git a/Functions/Zftp/zffcache b/Functions/Zftp/zffcache
new file mode 100644
index 000000000..0d9686660
--- /dev/null
+++ b/Functions/Zftp/zffcache
@@ -0,0 +1,24 @@
+# Generate an array name for storing the cache for the current session,
+# storing it in fcache_name, then generate the cache for the current
+# directory, or with argument -d clear the cache.
+
+fcache_name=$zfconfig[fcache_$ZFTP_SESSION]
+if [[ -z $fcache_name ]]; then
+  local vals
+  vals=(${(v)zfconfig[(I)fcache_*]##zftp_fcache_})
+  integer i
+  while [[ -n ${vals[(r)zftp_fcache_$i]} ]]; do
+    (( i++ ))
+  done
+  fcache_name=zftp_fcache_$i
+  zfconfig[fcache_$ZFTP_SESSION]=$fcache_name
+fi
+
+if [[ $1 = -d ]]; then
+  unset $fcache_name
+elif (( ${(P)#fcache_name} == 0 )); then
+  local tmpf=${TMPPREFIX}zffcache$$
+  zftp ls >$tmpf
+  eval "$fcache_name=(\${(f)\"\$(<\$tmpf)\"})"
+  rm -f $tmpf
+fi
diff --git a/Functions/Zftp/zfgcp b/Functions/Zftp/zfgcp
index 26a08697d..916a5f7b6 100644
--- a/Functions/Zftp/zfgcp
+++ b/Functions/Zftp/zfgcp
@@ -16,30 +16,16 @@
 
 emulate -L zsh
 
-local opt optlist nglob remlist rem loc time
+local opt remlist rem loc opt_G opt_t
 integer stat do_close
 
-while [[ $1 == -* ]]; do
-  if [[ $1 == - || $1 == -- ]]; then
-    shift;
-    break;
-  fi
-  optlist=${1#-}
-  for (( i = 1; i <= $#optlist; i++)); do
-    opt=$optlist[$i]
-    case $opt in
-      G) nglob=1
-	 ;;
-      t) time=1
-	 ;;
-      *) print option $opt not recognised >&2
-	 ;;
-    esac
-  done
-  shift
+while getopts :Gt opt; do
+  [[ $opt = '?' ]] && print "zfgcp: bad option: -$OPTARG" >&2 && return 1
+  eval "opt_$opt=1"
 done
+(( OPTIND > 1 )) && shift $(( OPTIND - 1 ))
 
-zfautocheck
+zfautocheck || return 1
 
 # hmm, we should really check this after expanding the glob,
 # but we shouldn't expand the last argument remotely anyway.
@@ -59,14 +45,14 @@ if [[ -d $argv[-1] ]]; then
     if [[ $remlist = $HOME || $remlist = $HOME/* ]]; then
       remlist="~${remlist#$HOME}"
     fi
-    if [[ $nglob != 1 ]]; then
+    if [[ $opt_G != 1 ]]; then
       zfrglob remlist
     fi
     if (( $#remlist )); then
       for rem in $remlist; do
 	loc=$dir/${rem:t}
 	if zftp get $rem >$loc; then
-	  [[ $time = 1 ]] && zfrtime $rem $loc
+	  [[ $opt_t = 1 ]] && zfrtime $rem $loc
 	else
 	  stat=1
 	fi
@@ -74,6 +60,9 @@ if [[ -d $argv[-1] ]]; then
     fi
   done
 else
+  if [[ $1 = $HOME || $1 = $HOME/* ]]; then
+    1="~${1#$HOME}"
+  fi
   zftp get $1 >$2 || stat=$?
 fi
 
diff --git a/Functions/Zftp/zfget b/Functions/Zftp/zfget
index cee0290b3..cb058204d 100644
--- a/Functions/Zftp/zfget
+++ b/Functions/Zftp/zfget
@@ -19,50 +19,34 @@
 
 emulate -L zsh
 
-local loc rem optlist opt nglob remlist time cat
+local loc rem opt remlist opt_G opt_t opt_c
 integer stat do_close
 
-while [[ $1 == -* ]]; do
-  if [[ $1 == - || $1 == -- ]]; then
-    shift;
-    break;
-  fi
-  optlist=${1#-}
-  for (( i = 1; i <= $#optlist; i++)); do
-    opt=$optlist[$i]
-    case $opt in
-      G) nglob=1
-	 ;;
-      t) time=1
-	 ;;
-      c) cat=1
-	 ;;
-      *) print option $opt not recognised >&2
-	 ;;
-    esac
-  done
-  shift
+while getopts :Gtc opt; do
+  [[ $opt = '?' ]] && print "zfget: bad option: -$OPTARG" && return 1
+  eval "opt_$opt=1"
 done
+(( OPTIND > 1 )) && shift $(( OPTIND - 1 ))
 
-zfautocheck
+zfautocheck || return 1
 
 for remlist in $*; do
   # zfcd directory hack to put the front back to ~
   if [[ $remlist == $HOME || $remlist == $HOME/* ]]; then
     remlist="~${remlist#$HOME}"
   fi
-  if [[ $nglob != 1 ]]; then
+  if [[ $opt_G != 1 ]]; then
     zfrglob remlist
   fi
   if (( $#remlist )); then
     for rem in $remlist; do
-      if [[ -n $cat ]]; then
+      if [[ -n $opt_c ]]; then
 	zftp get $rem
 	stat=$?
       else
 	loc=${rem:t}
 	if zftp get $rem >$loc; then
-	  [[ $time = 1 ]] && zfrtime $rem $loc
+	  [[ $opt_t = 1 ]] && zfrtime $rem $loc
 	else
 	  stat=1
 	fi
diff --git a/Functions/Zftp/zfget_match b/Functions/Zftp/zfget_match
index 677108ede..875fca5f1 100644
--- a/Functions/Zftp/zfget_match
+++ b/Functions/Zftp/zfget_match
@@ -10,18 +10,29 @@ fi
 local tmpf=${TMPPREFIX}zfgm$$
 
 if [[ $ZFTP_SYSTEM == UNIX* && $1 == */* ]]; then
-  # On the first argument to ls, we usually get away with a glob.
-  zftp ls "$1*$2" >$tmpf
-  reply=($(<$tmpf))
-  rm -f $tmpf
-else
-  if (( $#zftp_fcache == 0 )); then
-    # Always cache the current directory and use it
-    # even if the system is UNIX.
-    zftp ls >$tmpf
-    zftp_fcache=($(<$tmpf))
+  if [[ -n $WIDGET ]]; then
+    local dir=${1:h}
+    [[ $dir = */ ]] || dir="$dir/"
+    zftp ls -LF $dir >$tmpf
+    local reply
+    reply=(${${${(f)"$(<$tmpf)"}##$dir}%\*})
+    rm -f $tmpf
+    _description expl 'remote file'
+    compadd "$expl[@]" -P $dir - $reply
+  else
+    # On the first argument to ls, we usually get away with a glob.
+    zftp ls "$1*$2" >$tmpf
+    reply=($(<$tmpf))
     rm -f $tmpf
   fi
-  reply=($zftp_fcache);
+else
+  local fcache_name
+  zffcache
+  if [[ -n $WIDGET ]]; then
+    _description expl 'remote file'
+    compadd "$expl[@]" -F fignore - ${(P)fcache_name}
+  else
+    reply=(${(P)fcache_name});
+  fi
 fi
 # }
diff --git a/Functions/Zftp/zfgoto b/Functions/Zftp/zfgoto
index bd1cdbfe5..57651e383 100644
--- a/Functions/Zftp/zfgoto
+++ b/Functions/Zftp/zfgoto
@@ -2,6 +2,9 @@
 # Go to bookmark bname, a location on a remote FTP host.  Unless
 # this was the last session or is for anonymous FTP, prompt for
 # the user's password.
+#
+# Maybe this should try and look for an appropriate session to use
+# for the transfer.
 
 emulate -L zsh
 setopt extendedglob
@@ -11,33 +14,20 @@ setopt extendedglob
 : ${ZFTP_BMFILE:=${ZFDOTDIR:-$HOME}/.zfbkmarks}
 
 typeset -A bkmarks
-local line ncftp opt optlist
+local line opt_n opt
 
-while [[ $1 == -* ]]; do
-  if [[ $1 == - || $1 == -- ]]; then
-    shift;
-    break;
-  fi
-  optlist=${1#-}
-  for (( i = 1; i <= $#optlist; i++)); do
-    opt=$optlist[$i]
-    case $opt in
-      n) ncftp=1
-	 ;;
-      *) print option $opt not recognised >&2
-	 return 1
-	 ;;
-    esac
-  done
-  shift
+while getopts :n opt; do
+  [[ $opt = '?' ]] && print "zfgoto: bad option: -$OPTARG" && return 1
+  eval "opt_$opt=1"
 done
+(( OPTIND > 1 )) && shift $(( OPTIND - 1 ))
 
 if (( $# != 1 )); then
   print "Usage: zfgoto bookmark" >&2
   return 1
 fi
 
-if [[ -n $ncftp && -f ~/.ncftp/bookmarks ]]; then
+if [[ -n $opt_n && -f ~/.ncftp/bookmarks ]]; then
   local oldifs=$IFS
   IFS=,
   while read -rA line; do
@@ -73,7 +63,8 @@ if [[ $ZFTP_USER = $user && $ZFTP_HOST = $host ]]; then
 elif [[ $user = ftp || $user = anonymous ]]; then
   # Anonymous ftp, so we don't need password etc.
   zfanon $host && [[ -n $dir ]] && zfcd $dir
-elif [[ $zflastsession = ${host}:* && $user = $zflastuser ]]; then
+elif [[ $zfconfig[lastloc_$ZFTP_SESSION] = ${host}:* &&
+  $user = $zfconfig[lastuser_$ZFTP_SESSION] ]]; then
   # This was the last session, so assume it's still setup in the
   # open parameters
   zfopen && [[ -n $dir ]] && zfcd $dir
diff --git a/Functions/Zftp/zfinit b/Functions/Zftp/zfinit
index 0bc619277..f650b7bbb 100644
--- a/Functions/Zftp/zfinit
+++ b/Functions/Zftp/zfinit
@@ -4,7 +4,7 @@ emulate -L zsh
 
 if [[ ${+zfconfig} = 0 ]]; then
   typeset -gA zfconfig
-  zfconfig=(progress bar update 1)
+  zfconfig=(progress bar update 1 lastsession default)
 fi
 
 alias zfcd='noglob zfcd'
@@ -14,9 +14,9 @@ alias zfdir='noglob zfdir'
 alias zfuget='noglob zfuget'
 
 autoload -U zfanon zfautocheck zfcd zfcd_match zfcget zfclose zfcput
-autoload -U zfdir zfgcp zfget zfget_match zfgoto zfhere zfinit zfls
-autoload -U zfmark zfopen zfparams zfpcp zfput zfrglob zfrtime zfstat
-autoload -U zftp_chpwd zftp_progress zftype zfuget zfuput
+autoload -U zfdir zffcache zfgcp zfget zfget_match zfgoto zfhere zfinit zfls
+autoload -U zfmark zfopen zfparams zfpcp zfput zfrglob zfrtime zfsession
+autoload -U zfstat zftp_chpwd zftp_progress zftransfer zftype zfuget zfuput
 
 #
 # zftp completions: only use these if new-style completion is not
@@ -33,7 +33,8 @@ if [[ ${#_patcomps} -eq 0 || ${_patcomps[(i)zf*]} -gt ${#_patcomps} ]]; then
     close quit)'  - \
     'w[1,cd][1,ls][1,dir][1,rmdir]' -K zfcd_match -S/ -q - \
     'W[1,get*]' -K zfget_match - 'w[1,delete][1,remote]' -K zfget_match - \
-    'w[1,open][1,params]' -k hosts -- zftp
+    'w[1,open][1,params]' -k hosts - \
+    'w[1,session]' -s '${$(zftp session):#$ZFTP_SESSION}' -- zftp
   compctl -K zfcd_match -S/ -q zfcd zfdir zfls
   compctl -K zfget_match zfget zfgcp zfuget zfcget
   compctl -k hosts zfanon zfopen zfparams
@@ -42,4 +43,15 @@ if [[ ${#_patcomps} -eq 0 || ${_patcomps[(i)zf*]} -gt ${#_patcomps} ]]; then
     -x 'W[1,-*n*]' \
     -s '$(awk -F, '\''NR > 2 { print $1 }'\'' ~/.ncftp/bookmarks)' -- \
     zfgoto zfmark
+  compctl -s '${$(zftp session):#$ZFTP_SESSION}' -- zfsession
+  # in _zftp for new completion, but hard to inline into a compctl
+  zftransfer_match() {
+    local sess=${1%%:*} oldsess=$ZFTP_SESSION
+    [[ -n $sess ]] && zftp session $sess
+    zfget_match ${1#*:} $2
+    [[ -n $sess && -n $oldsess ]] && zftp session $oldsess
+    reply=(${sess}:${^reply})
+  }
+  compctl -s '$(zftp session)' -S : -x 'C[0,*:*]' \
+    -K zftransfer_match -- zftransfer
 fi
diff --git a/Functions/Zftp/zfmark b/Functions/Zftp/zfmark
index 8d35ce45a..74cc702ac 100644
--- a/Functions/Zftp/zfmark
+++ b/Functions/Zftp/zfmark
@@ -37,8 +37,9 @@ fi
 
 if [[ -n $ZFTP_HOST ]]; then
   bkmarks[$1]="${ZFTP_USER}@${ZFTP_HOST}:${ZFTP_PWD}"
-elif [[ -n $zflastsession ]]; then
-  bkmarks[$1]="${zflastuser}@${zflastsession}"
+elif [[ -n $zfconfig[lastloc_$ZFTP_SESSION] ]]; then
+  bkmarks[$1]="${zfconig[lastuser_$ZFTP_SESSION]}@\
+${zfconfig[lastloc_$ZFTP_SESSION]}"
 else
   print "No current or recent ZFTP session to bookmark." >&2
   return 1
diff --git a/Functions/Zftp/zfopen b/Functions/Zftp/zfopen
index b264aeaba..32b450411 100644
--- a/Functions/Zftp/zfopen
+++ b/Functions/Zftp/zfopen
@@ -7,25 +7,13 @@
 
 emulate -L zsh
 
-local optlist opt once dir
+local opt dir opt_1
 
-while [[ $1 = -* ]]; do
-  if [[ $1 = - || $1 = -- ]]; then
-    shift;
-    break;
-  fi
-  optlist=${1#-}
-  for (( i = 1; i <= $#optlist; i++)); do
-    opt=$optlist[$i]
-    case $optlist[$i] in
-      1) once=1
-	 ;;
-      *) print option $opt not recognised >&2
-	 ;;
-    esac
-  done
-  shift
+while getopts :1 opt; do
+  [[ $opt = "?" ]] && print "zfopen: bad option: -$OPTARG" >&2 && return 1
+  eval "opt_$opt=1"
 done
+(( OPTIND > 1 )) && shift $(( OPTIND - 1 ))
 
 # This is where we should try and do same name-lookupage in
 # both .netrc and .ncftp/bookmarks .  We could even try saving
@@ -37,8 +25,14 @@ if [[ $1 = */* ]]; then
   1=${1%%/*}
 fi
 
-if [[ $once = 1 ]]; then
+if [[ $opt_1 = 1 ]]; then
   zftp open $* || return 1
+  if [[ $# = 1 ]]; then
+    if ! zftp login; then
+      zftp close
+      return 1
+    fi
+  fi
 else
   # set parameters, but only if there was at least a host
   (( $# > 0 )) && zfparams $*
diff --git a/Functions/Zftp/zfparams b/Functions/Zftp/zfparams
index 5c5262c52..faae63251 100644
--- a/Functions/Zftp/zfparams
+++ b/Functions/Zftp/zfparams
@@ -2,11 +2,25 @@
 
 emulate -L zsh
 
-# Set to prompt for any user or password if not given.
-# Don't worry about accounts here.
-if (( $# > 0 )); then
+if [[ $# -eq 1 && $1 = - ]]; then
+  # Delete existing parameter set.
+  local sess=$ZFTP_SESSION key
+  key=${zfconfig[fcache_$sess]}
+  [[ -n $key ]] && unset $key
+  for key in fcache lastloc lastdir curdir otherdir otherargs lastuser; do
+    unset "zfconfig[${key}_${sess}]"
+  done
+elif (( $# > 0 )); then
+  # Set to prompt for any user or password if not given.
+  # Don't worry about accounts here.
   (( $# < 2 )) && 2='?'
-  (( $# < 3 )) && 3='?'
+  if (( $# < 3 )); then
+    if [[ $2 = '?'* ]]; then
+      3="?Password on ${1}: "
+    else
+      3="?Password for ${2##\\?} on ${1}: "
+    fi
+  fi
 fi
 zftp params $*
 # }
diff --git a/Functions/Zftp/zfpcp b/Functions/Zftp/zfpcp
index ddd570e59..9642688b7 100644
--- a/Functions/Zftp/zfpcp
+++ b/Functions/Zftp/zfpcp
@@ -17,7 +17,7 @@ emulate -L zsh
 local rem loc
 integer stat do_close
 
-zfautocheck
+zfautocheck || return 1
 
 if [[ $# -gt 2 || $2 = (.|..) || $2 = */ ]]; then
   local dir=$argv[-1]
@@ -32,6 +32,9 @@ if [[ $# -gt 2 || $2 = (.|..) || $2 = */ ]]; then
     zftp put $rem <$loc || stat=1
   done
 else
+  if [[ $2 = $HOME || $2 = $HOME/* ]]; then
+    2="~${2#$HOME}"
+  fi
   zftp put $2 <$1
   stat=$?
   if [[ stat -ne 0 && $ZFTP_CODE = 553 && $ZFTP_REPLY = *'Is a directory'* ]]
diff --git a/Functions/Zftp/zfrglob b/Functions/Zftp/zfrglob
index fad0c3849..535cb8006 100644
--- a/Functions/Zftp/zfrglob
+++ b/Functions/Zftp/zfrglob
@@ -57,14 +57,10 @@ else
     rm -f $tmpf
   else
     # we just have to do an ls and hope that's right
+    local fcache_name
+    zffcache
     nondir=$pat
-    if (( $#zftp_fcache == 0 )); then
-      # Why does `zftp_fcache=($(zftp ls))' sometimes not work?
-      zftp ls >$tmpf
-      zftp_fcache=($(<$tmpf))
-      rm -f $tmpf
-    fi
-    files=($zftp_fcache)
+    files=(${(P)fcache_name})
   fi
   # now we want to see which of the $files match $nondir:
   # ${...:/foo} deletes occurrences of foo matching a complete word,
diff --git a/Functions/Zftp/zfsession b/Functions/Zftp/zfsession
new file mode 100644
index 000000000..9cd0d918f
--- /dev/null
+++ b/Functions/Zftp/zfsession
@@ -0,0 +1,71 @@
+# function zfsession {
+# Change or list the sessions for the current zftp connection.
+
+emulate -L zsh
+
+local opt opt_l opt_v opt_o opt_d hadopts
+
+while getopts ":lovd" opt; do
+  [[ $opt = "?" ]] && print "zfsession: bad option: -$OPTARG" >&2 && return 1
+  eval "opt_$opt=1"
+  hadopts=1
+done
+(( OPTIND > 1 )) && shift $(( OPTIND - 1 ))
+
+if [[ $# -gt 1 || (( -n $hadopts && -z $opt_d ) && $# -gt 0 ) ]]
+then
+  print "Usage: zfsession ( [ -lvod ] | session )" 1>&2
+  return 1
+fi
+
+if [[ -n $opt_v ]]; then
+  local sess
+  for sess in $(zftp session); do
+    print -n "${(r.15.. ..:.)sess}\t${zfconfig[lastloc_$sess]:-not connected}"
+    if [[ $sess = $ZFTP_SESSION ]]; then
+      print " *"
+    else
+      print
+    fi
+  done
+elif [[ -n $opt_l ]]; then
+  zftp session
+fi
+
+if [[ -n $opt_o ]]; then
+  if [[ $zfconfig[lastsession] != $ZFTP_SESSION ]]; then
+    local cursession=$ZFTP_SESSION
+    zftp session $zfconfig[lastsession]
+    zfconfig[lastsession]=$cursession
+    print $ZFTP_SESSION
+  else
+    print "zfsession: no previous session." >&2
+    return 1
+  fi
+fi
+
+if [[ -n $opt_d ]]; then
+  local del=${1:-$ZFTP_SESSION} key
+  key=${zfconfig[fcache_$del]}
+  [[ -n $key ]] && unset $key
+  for key in fcache lastloc lastdir curdir otherdir otherargs lastuser; do
+    unset "zfconfig[${key}_${del}]"
+  done
+  zftp rmsession $del
+  return
+fi
+
+[[ -n $hadopts ]] && return $stat
+
+if [[ $# = 0 ]]; then
+  print $ZFTP_SESSION
+  return
+fi
+
+local oldsession=${ZFTP_SESSION:-default}
+zftp session $1
+if [[ $ZFTP_SESSION != $oldsession ]]; then
+  zfconfig[lastsession]=$oldsession
+  zftp_chpwd
+fi
+# }
diff --git a/Functions/Zftp/zfstat b/Functions/Zftp/zfstat
index 0ca755d03..6945da99b 100644
--- a/Functions/Zftp/zfstat
+++ b/Functions/Zftp/zfstat
@@ -6,25 +6,15 @@
 setopt localoptions unset
 unsetopt ksharrays
 
-local i stat=0 opt optlist verbose
+local i stat=0 opt opt_v
 
-while [[ $1 = -* ]]; do
-  if [[ $1 = - || $1 = -- ]]; then
-    shift;
-    break;
-  fi
-  optlist=${1#-}
-  for (( i = 1; i <= $#optlist; i++)); do
-    opt=$optlist[$i]
-    case $opt in
-      v) verbose=1
-	 ;;
-      *) print option $opt not recognised >&2
-	 ;;
-    esac
-  done
-  shift
+while getopts :v opt; do
+  [[ $opt = "?" ]] && print "zfstat: bad option: -$OPTARG" >&2 && return 1
+  eval "opt_$opt=1"
 done
+(( OPTIND > 1 )) && shift $(( OPTIND - 1 ))
+
+[[ -n $ZFTP_SESSION ]] && print "Session:\t$ZFTP_SESSION"
 
 if [[ -n $ZFTP_HOST ]]; then
   print "Host:\t\t$ZFTP_HOST"
@@ -55,7 +45,8 @@ if [[ -n $ZFTP_HOST ]]; then
   fi
 else
   print "Not connected."
-  [[ -n $zflastsession ]] && print "Last session:\t$zflastsession"
+  [[ -n $zfconfig[lastloc_$ZFTP_SESSION] ]] &&
+  print "Last location:\t$zfconfig[lastloc_$ZFTP_SESSION]"
   stat=1
 fi
 
@@ -77,7 +68,7 @@ for (( i = 1; i <= ${#ZFTP_PREFS}; i++ )); do
 done
 print
 
-if [[ -n $ZFTP_HOST && $verbose = 1 ]]; then
+if [[ -n $ZFTP_HOST && $opt_v = 1 ]]; then
   zfautocheck -d
   print "Status of remote server:"
   # make sure we print the reply
diff --git a/Functions/Zftp/zftp_chpwd b/Functions/Zftp/zftp_chpwd
index 0b5bbd7d5..53972cd65 100644
--- a/Functions/Zftp/zftp_chpwd
+++ b/Functions/Zftp/zftp_chpwd
@@ -2,14 +2,14 @@
 # You may want to alter chpwd to call this when $ZFTP_USER is set.
 
 # If the directory really changed...
-if [[ $ZFTP_PWD != $zflastdir ]]; then
-  # Cancel the filename cache for the current directory.
-  zftp_fcache=()
+if [[ $ZFTP_PWD != $zfconfig[lastdir_$ZFTP_SESSION] ]]; then
   # ...and also empty the stored directory listing cache.
   # As this function is called when we close the connection, this
   # is the only place we need to do these two things.
-  [[ -n $zfcurdir && -f $zfcurdir ]] && rm -f $zfcurdir
-  zfotherargs=
+  local curdir=$zfconfig[curdir_$ZFTP_SESSION]
+  [[ -n $curdir && -f $curdir ]] && rm -f $curdir
+  zfconfig[otherargs_$ZFTP_SESSION]=
+  zffcache -d
 fi
 
 if [[ -z $ZFTP_USER ]]; then
@@ -18,24 +18,24 @@ if [[ -z $ZFTP_USER ]]; then
   # delete the non-current cached directory
   [[ -n $zfotherdir && -f $zfotherdir ]] && rm -f $zfotherdir
 
-  # don't keep zflastdir between opens (do keep zflastsession)
-  zflastdir=
+  # don't keep lastdir between opens (do keep lastloc)
+  zfconfig[lastdir_$ZFTP_SESSION]=
 
   # return the display to standard
   # uncomment the following line if you have a chpwd which shows directories
-  # chpwd
+  chpwd
 else
-  [[ -n $ZFTP_PWD ]] && zflastdir=$ZFTP_PWD
-  zflastsession="$ZFTP_HOST:$ZFTP_PWD"
-  zflastuser="$ZFTP_USER"
+  [[ -n $ZFTP_PWD ]] && zfconfig[lastdir_$ZFTP_SESSION]=$ZFTP_PWD
+  zfconfig[lastloc_$ZFTP_SESSION]="$ZFTP_HOST:$ZFTP_PWD"
+  zfconfig[lastuser_$ZFTP_SESSION]="$ZFTP_USER"
   local args
   if [[ -t 1 && -t 2 ]]; then
-    local str=$zflastsession
+    local str=$zfconfig[lastloc_$ZFTP_SESSION]
     [[ ${#str} -lt 70 ]] && str="%m: %~  $str"
     case $TERM in
       sun-cmd) print -n -P "\033]l$str\033\\"
 	       ;;
-      xterm) print -n -P "\033]2;$str\a"
+      xterm|aixterm) print -n -P "\033]2;$str\a"
 	     ;;
     esac
   fi
diff --git a/Functions/Zftp/zftp_progress b/Functions/Zftp/zftp_progress
index 344d1c9c1..b4b639fce 100644
--- a/Functions/Zftp/zftp_progress
+++ b/Functions/Zftp/zftp_progress
@@ -26,17 +26,19 @@ if [[ -n $ZFTP_TRANSFER ]]; then
   # avoid a `parameter unset' message
   [[ $ZFTP_TRANSFER != *F ]] &&
     (( ${+zftpseconds} )) && (( SECONDS - zftpseconds < update )) && return
-  if [[ -n $ZFTP_SIZE ]]; then
-    local frac="$(( ZFTP_COUNT * 100 / ZFTP_SIZE ))%"
+  # size is usually ZFTP_SIZE, but zftransfer may set ZFTP_TSIZE
+  local size=${ZFTP_TSIZE:-$ZFTP_SIZE}
+  if [[ -n $size ]]; then
+    local frac="$(( ZFTP_COUNT * 100 / size ))%"
     if [[ $style = bar && ${+COLUMNS} = 1 && $COLUMNS -gt 0 ]]; then
       if (( ! ${+zftpseconds} )); then
-	print "$ZFTP_FILE ($ZFTP_SIZE bytes): $ZFTP_TRANSFER" 1>&2
+	print "$ZFTP_FILE ($size bytes): $ZFTP_TRANSFER" 1>&2
       fi
       integer maxwidth=$(( COLUMNS - 7 ))
-      local width="$(( ZFTP_COUNT * maxwidth / ZFTP_SIZE ))"
+      local width="$(( ZFTP_COUNT * maxwidth / size ))"
       print -nP "\r%S${(l:width:):-}%s${(l:maxwidth-width:):-}: ${frac}%%" 1>&2
     else
-      print -n "\r$ZFTP_FILE ($ZFTP_SIZE bytes): $ZFTP_TRANSFER $frac" 1>&2
+      print -n "\r$ZFTP_FILE ($size bytes): $ZFTP_TRANSFER $frac" 1>&2
     fi
   else
     print -n "\r$ZFTP_FILE: $ZFTP_TRANSFER $ZFTP_COUNT" 1>&2
diff --git a/Functions/Zftp/zftransfer b/Functions/Zftp/zftransfer
new file mode 100644
index 000000000..929f099d2
--- /dev/null
+++ b/Functions/Zftp/zftransfer
@@ -0,0 +1,62 @@
+# function zftransfer {
+# Transfer files between two distinct sessions. No remote globbing
+# is done, since only single pairs can be transferred.
+
+emulate -L zsh
+
+local sess1 sess2 file1 file2 oldsess=${ZFTP_SESSION}
+
+if [[ $# -ne 2 ]]; then
+  print "Usage: zftransfer sess1:file1 sess2:file2" 1>&2
+  return 1
+fi
+
+if [[ $1 = *:* ]]; then
+  sess1=${1%%:*}
+  file1=${1#*:}
+fi
+: ${sess1:=$ZFTP_SESSION}
+
+if [[ $2 = *:* ]]; then
+  sess2=${2%%:*}
+  file2=${2#*:}
+fi
+: ${sess2:=$ZFTP_SESSION}
+if [[ -z $file2 || $file2 = */ ]]; then
+  file2="${file2}${file1:t}"
+fi
+
+if [[ $sess1 = $sess2 ]]; then
+  print "zftransfer: must use two distinct sessions." 1>&2
+  return 1
+fi
+
+zftp session $sess1
+zfautocheck || return 1
+
+# It's more useful to show the progress for the second part
+# of the pipeline, but unfortunately that can't necessarily get
+# the size from the pipe --- and if it does, it's probably wrong.
+# To avoid that, try to get the size and set it for the progress to
+# see.
+if [[ $zfconfig[progress] != none ]]; then
+  local ZFTP_TSIZE array tmpfile=${TMPPREFIX}zft$$
+  zftp remote $file1 >$tmpfile 2>/dev/null
+  array=($(<$tmpfile))
+  rm -f $tmpfile
+  [[ $#array -eq 2 ]] && ZFTP_TSIZE=$array[1]
+fi
+
+# We do the RHS of the pipeline in a subshell, too, so that
+# the LHS can get SIGPIPE when it exits.
+{ zfconfig[progress]=none
+  zftp get $file1 } |
+( zftp session $sess2
+  zfautocheck && zftp put $file2 )
+
+local stat=$?
+
+zftp session $oldsess
+
+return $stat
+# }
diff --git a/Functions/Zftp/zfuget b/Functions/Zftp/zfuget
index 482da42e9..955c48f4a 100644
--- a/Functions/Zftp/zfuget
+++ b/Functions/Zftp/zfuget
@@ -26,7 +26,7 @@
 emulate -L zsh
 
 local loc rem locstats remstats doit tmpfile=${TMPPREFIX}zfuget$$
-local rstat remlist verbose optlist opt bad i silent nglob time
+local rstat remlist opt opt_v opt_s opt_G opt_t
 integer stat do_close
 
 zfuget_print_time() {
@@ -43,40 +43,20 @@ zfuget_print () {
   print ", $locstats[1] bytes)"
 }
 
-while [[ $1 = -* ]]; do
-  if [[ $1 = - || $1 = -- ]]; then
-    shift;
-    break;
-  fi
-  optlist=${1#-}
-  for (( i = 1; i <= $#optlist; i++)); do
-    opt=$optlist[$i]
-    case $optlist[$i] in
-      v) verbose=1
-	 ;;
-      s) silent=1
-	 ;;
-      G) nglob=1
-	 ;;
-      t) time=1
-	 ;;
-      *) print option $opt not recognised >&2
-	 ;;
-    esac
-  done
-  shift
+while getopts :vsGt opt; do
+  [[ $opt = "?" ]] && print "zfuget: bad option: -$OPTARG" >&2 && return 1
+  eval "opt_$opt=1"
 done
+(( OPTIND > 1 )) && shift $(( OPTIND - 1 ))
 
-[[ -n $bad ]] && return 1
-
-zfautocheck
+zfautocheck || return 1
 
 for remlist in $*; do
   # zfcd directory hack to put the front back to ~
   if [[ $remlist == $HOME || $remlist == $HOME/* ]]; then
     remlist="~${remlist#$HOME}"
   fi
-  if [[ $nglob != 1 ]]; then
+  if [[ $opt_n != 1 ]]; then
     zfrglob remlist
   fi
   if (( $#remlist )); then
@@ -99,11 +79,11 @@ for remlist in $*; do
 	  stat=1
 	  continue
 	fi
-	[[ $verbose = 1 ]] && zfuget_print
+	[[ $opt_v = 1 ]] && zfuget_print
 	if (( $locstats[1] != $remstats[1] )); then
 	  # Files have different sizes
-	  if [[ $locstats[2] > $remstats[2] && $silent != 1 ]]; then
-	    [[ $verbose != 1 ]] && zfuget_print
+	  if [[ $locstats[2] > $remstats[2] && $opt_s != 1 ]]; then
+	    [[ $opt_v != 1 ]] && zfuget_print
 	    print "Local file $loc more recent than remote," 1>&2
 	    print -n "but sizes are different.  Transfer anyway [y/n]? " 1>&2
 	    read -q doit
@@ -111,24 +91,24 @@ for remlist in $*; do
 	else
 	  # Files have same size
 	  if [[ $locstats[2] < $remstats[2] ]]; then
-	    if [[ $silent != 1 ]]; then
-	      [[ $verbose != 1 ]] && zfuget_print
+	    if [[ $opt_s != 1 ]]; then
+	      [[ $opt_v != 1 ]] && zfuget_print
 	      print "Local file $loc has same size as remote," 1>&2
 	      print -n "but local file is older. Transfer anyway [y/n]? " 1>&2
 	      read -q doit
 	    fi
 	  else
 	    # presumably same file, so don't get it.
-	    [[ $verbose = 1 ]] && print Not transferring
+	    [[ $opt_v = 1 ]] && print Not transferring
 	    doit=n
 	  fi
 	fi
       else
-	[[ $verbose = 1 ]] && print New file $loc
+	[[ $opt_v = 1 ]] && print New file $loc
       fi
       if [[ $doit = y ]]; then
 	if zftp get $rem >$loc; then
-	  if [[ $time = 1 ]]; then
+	  if [[ $opt_t = 1 ]]; then
 	    # if $remstats is set, it's second element is the remote time
 	    zfrtime $loc $rem $remstats[2]
 	  fi
diff --git a/Functions/Zftp/zfuput b/Functions/Zftp/zfuput
index b54d0d0d4..cb179052c 100644
--- a/Functions/Zftp/zfuput
+++ b/Functions/Zftp/zfuput
@@ -12,7 +12,7 @@
 emulate -L zsh
 
 local loc rem locstats remstats doit tmpfile=${TMPPREFIX}zfuput$$
-local rstat verbose optlist opt bad i silent
+local rstat opt opt_v opt_s
 integer stat do_close
 
 zfuput_print_time() {
@@ -29,29 +29,13 @@ zfuput_print () {
   print ", $locstats[1] bytes)"
 }
 
-while [[ $1 = -* ]]; do
-  if [[ $1 = - || $1 = -- ]]; then
-    shift;
-    break;
-  fi
-  optlist=${1#-}
-  for (( i = 1; i <= $#optlist; i++)); do
-    opt=$optlist[$i]
-    case $optlist[$i] in
-      v) verbose=1
-	 ;;
-      s) silent=1
-	 ;;
-      *) print option $opt not recognised >&2
-	 ;;
-    esac
-  done
-  shift
+while getopts :vs opt; do
+  [[ $opt = "?" ]] && print "zfuget: bad option: -$OPTARG" >&2 && return 1
+  eval "opt_$opt=1"
 done
+(( OPTIND > 1 )) && shift $(( OPTIND - 1 ))
 
-[[ -n $bad ]] && return 1
-
-zfautocheck
+zfautocheck || return 1
 
 if [[ $ZFTP_VERBOSE = *5* ]]; then
   # should we turn it off locally?
@@ -77,13 +61,13 @@ for rem in $*; do
     print "Server does not implement full command set required." 1>&2
     return 1
   elif [[ $rstat = 1 ]]; then
-    [[ $verbose = 1 ]] && print New file $loc
+    [[ $opt_v = 1 ]] && print New file $loc
   else
-    [[ $verbose = 1 ]] && zfuput_print
+    [[ $opt_v = 1 ]] && zfuput_print
     if (( $locstats[1] != $remstats[1] )); then
       # Files have different sizes
-      if [[ $locstats[2] < $remstats[2] && $silent != 1 ]]; then
-	[[ $verbose != 1 ]] && zfuput_print
+      if [[ $locstats[2] < $remstats[2] && $opt_s != 1 ]]; then
+	[[ $opt_v != 1 ]] && zfuput_print
 	print "Remote file $rem more recent than local," 1>&2
 	print -n "but sizes are different.  Transfer anyway [y/n]? " 1>&2
 	read -q doit
@@ -91,15 +75,15 @@ for rem in $*; do
     else
       # Files have same size
       if [[ $locstats[2] > $remstats[2] ]]; then
-	if [[ $silent != 1 ]]; then
-	  [[ $verbose != 1 ]] && zfuput_print
+	if [[ $opt_s != 1 ]]; then
+	  [[ $opt_v != 1 ]] && zfuput_print
 	  print "Remote file $rem has same size as local," 1>&2
 	  print -n "but remote file is older. Transfer anyway [y/n]? " 1>&2
 	  read -q doit
 	fi
       else
 	# presumably same file, so don't get it.
-	[[ $verbose = 1 ]] && print Not transferring
+	[[ $opt_v = 1 ]] && print Not transferring
 	doit=n
       fi
     fi
diff --git a/Src/Modules/zftp.c b/Src/Modules/zftp.c
index 738d596fa..37a549a63 100644
--- a/Src/Modules/zftp.c
+++ b/Src/Modules/zftp.c
@@ -29,27 +29,23 @@
 
 /*
  * TODO:
+ *   should be eight-bit clean, but isn't.
+ *   tracking of logical rather than physical directories, like nochaselinks
+ *     (usually PWD returns physical directory).
  *   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.
  */
+
+/* needed in prototypes for statics */
+struct sockaddr_in;
+struct zftp_session;
+typedef struct zftp_session *Zftp_session;
+
 #include "zftp.mdh"
 #include "zftp.pro"
 
@@ -138,7 +134,8 @@ enum {
     ZFTP_CDUP  = 0x0200,	/* CDUP rather than CWD */
     ZFTP_REST  = 0x0400,	/* restart: set point in remote file */
     ZFTP_RECV  = 0x0800,	/* receive rather than send */
-    ZFTP_TEST  = 0x1000		/* test command, don't test */
+    ZFTP_TEST  = 0x1000,	/* test command, don't test */
+    ZFTP_SESS  = 0x2000		/* session command, don't need status */
 };
 
 typedef struct zftpcmd *Zftpcmd;
@@ -173,7 +170,9 @@ static struct zftpcmd zftpcmdtab[] = {
     { "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}
+    { "session", zftp_session, 0, 1, ZFTP_SESS },
+    { "rmsession", zftp_rmsession, 0, 1, ZFTP_SESS },
+    { 0, 0, 0, 0, 0 }
 };
 
 static struct builtin bintab[] = {
@@ -198,13 +197,8 @@ enum {
     ZFPM_INTEGER  = 0x04	/* passed pointer to off_t */
 };
 
-/*
- * 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;
+/* Number of connections actually open */
+static int zfnopen;
 
 /*
  * zcfinish = 0 keep going
@@ -216,12 +210,6 @@ static int zcfinish;
 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.
@@ -229,9 +217,6 @@ static struct sockaddr_in zdsock;
 static char *lastmsg, lastcodestr[4];
 static int lastcode;
 
-/* flag for remote system is UNIX --- useful to know as things are simpler */
-static int zfpassive_conn;
-
 /* remote system has size, mdtm commands */
 enum {
     ZFCP_UNKN = 0,		/* dunno if it works on this server */
@@ -239,8 +224,6 @@ enum {
     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
@@ -279,7 +262,8 @@ enum {
 #define ZFST_CTYP(x) ((x >> ZFST_TBIT) & ZFST_TMSK)
 #define ZFST_MODE(x) (x & ZFST_MMSK)
 
-static int zfstatfd = -1, zfstatus;
+/* fd containing status for all sessions and array for internal use */
+static int zfstatfd = -1, *zfstatusp;
 
 /* Preferences, read in from the `zftp_prefs' array variable */
 enum {
@@ -289,11 +273,42 @@ enum {
 };
 
 /* The flags as stored internally. */
-int zfprefs;
+static int zfprefs;
 
+/*
+ * Data node for linked list of sessions.
+ *
+ * Memory management notes:
+ *   name is permanently allocated and remains for the life of the node.
+ *   userparams is set directly by zftp_params and also freed with the node.
+ *   params and its data are allocated when we need
+ *     to save an existing session, and are freed when we switch back
+ *     to that session.
+ *   The node itself is deleted when we remove it from the list.
+ */
+struct zftp_session {
+    char *name;			/* name of session */
+    char **params;		/* parameters ordered as in zfparams */
+    char **userparams;		/* user parameters set by zftp_params */
+    int cfd;			/* control file descriptor */
+    FILE *cin;			/* control input file */
+    struct sockaddr_in sock;	/* the socket for the control connection */
+    int dfd;			/* data connection */
+    int has_size;		/* understands SIZE? */
+    int has_mdtm;		/* understands MDTM? */
+};
+
+/* List of active sessions */
+static LinkList zfsessions;
+
+/* Current session */
+static Zftp_session zfsess;
+
+/* Number of current session, corresponding to position in list */
+static int zfsessno;
 
-/* zfuserparams is the storage area for zftp_params() */
-char **zfuserparams;
+/* Total number of sessions */
+static int zfsesscnt;
 
 /*
  * Bits and pieces for dealing with SIGALRM (and SIGPIPE, but that's
@@ -570,12 +585,12 @@ zfgetline(char *ln, int lnsize, int tmout)
      * But then we get `frustrated user syndrome'.
      */
     for (;;) {
-	ch = fgetc(zcin);
+	ch = fgetc(zfsess->cin);
 
 	switch(ch) {
 	case EOF:
-	    if (ferror(zcin) && errno == EINTR) {
-		clearerr(zcin);
+	    if (ferror(zfsess->cin) && errno == EINTR) {
+		clearerr(zfsess->cin);
 		continue;
 	    }
 	    zcfinish = 2;
@@ -583,7 +598,7 @@ zfgetline(char *ln, int lnsize, int tmout)
 
 	case '\r':
 	    /* always precedes something else */
-	    ch = fgetc(zcin);
+	    ch = fgetc(zfsess->cin);
 	    if (ch == EOF) {
 		zcfinish = 2;
 		break;
@@ -610,26 +625,26 @@ zfgetline(char *ln, int lnsize, int tmout)
 	     * oh great, now it's sending TELNET commands.  try
 	     * to persuade it not to.
 	     */
-	    ch = fgetc(zcin);
+	    ch = fgetc(zfsess->cin);
 	    switch (ch) {
 	    case WILL:
 	    case WONT:
-		ch = fgetc(zcin);
+		ch = fgetc(zfsess->cin);
 		/* whatever it wants to do, stop it. */
 		cmdbuf[0] = (char)IAC;
 		cmdbuf[1] = (char)DONT;
 		cmdbuf[2] = ch;
-		write(zcfd, cmdbuf, 3);
+		write(zfsess->cfd, cmdbuf, 3);
 		continue;
 
 	    case DO:
 	    case DONT:
-		ch = fgetc(zcin);
+		ch = fgetc(zfsess->cin);
 		/* well, tough, we're not going to. */
 		cmdbuf[0] = (char)IAC;
 		cmdbuf[1] = (char)WONT;
 		cmdbuf[2] = ch;
-		write(zcfd, cmdbuf, 3);
+		write(zfsess->cfd, cmdbuf, 3);
 		continue;
 
 	    case EOF:
@@ -679,7 +694,7 @@ zfgetmsg(void)
     char line[256], *ptr, *verbose;
     int stopit, printing = 0, tmout;
 
-    if (zcfd == -1)
+    if (zfsess->cfd == -1)
 	return 6;
     if (!(verbose = getsparam("ZFTP_VERBOSE")))
 	verbose = "";
@@ -695,7 +710,7 @@ zfgetmsg(void)
 	/* timeout, or not talking FTP.  not really interested. */
 	zcfinish = 2;
 	if (!zfclosing)
-	    zfclose();
+	    zfclose(0);
 	lastmsg = ztrdup("");
 	strcpy(lastcodestr, "000");
 	zfsetparam("ZFTP_REPLY", ztrdup(lastmsg), ZFPM_READONLY);
@@ -764,7 +779,7 @@ zfgetmsg(void)
      */
     if ((zcfinish == 2 || lastcode == 421) && !zfclosing) {
 	zcfinish = 2;		/* don't need to tell server */
-	zfclose();
+	zfclose(0);
 	/* unexpected, so tell user */
 	zwarnnam("zftp", "remote server has closed connection", NULL, 0);
 	return 6;
@@ -805,7 +820,7 @@ zfsendcmd(char *cmd)
      */
     int ret, tmout;
 
-    if (zcfd == -1)
+    if (zfsess->cfd == -1)
 	return 6;
     tmout = getiparam("ZFTP_TMOUT");
     if (setjmp(zfalrmbuf)) {
@@ -814,7 +829,7 @@ zfsendcmd(char *cmd)
 	return 6;
     }
     zfalarm(tmout);
-    ret = write(zcfd, cmd, strlen(cmd));
+    ret = write(zfsess->cfd, cmd, strlen(cmd));
     alarm(0);
 
     if (ret <= 0) {
@@ -831,22 +846,22 @@ zfsendcmd(char *cmd)
 
 /**/
 static int
-zfopendata(char *name)
+zfopendata(char *name, struct sockaddr_in *zdsockp, int *is_passivep)
 {
     if (!(zfprefs & (ZFPF_SNDP|ZFPF_PASV))) {
 	zwarnnam(name, "Must set preference S or P to transfer data", NULL, 0);
 	return 1;
     }
-    zdfd = socket(AF_INET, SOCK_STREAM, 0);
-    if (zdfd < 0) {
+    zfsess->dfd = socket(AF_INET, SOCK_STREAM, 0);
+    if (zfsess->dfd < 0) {
 	zwarnnam(name, "can't get data socket: %e", NULL, errno);
 	return 1;
     }
 
-    zdsock = zsock;
-    zdsock.sin_family = AF_INET;
+    *zdsockp = zfsess->sock;
+    zdsockp->sin_family = AF_INET;
 
-    if (!(zfstatus & ZFST_NOPS) && (zfprefs & ZFPF_PASV)) {
+    if (!(zfstatusp[zfsessno] & ZFST_NOPS) && (zfprefs & ZFPF_PASV)) {
 	char *ptr;
 	int i, nums[6], err;
 	unsigned char iaddr[4], iport[2];
@@ -858,9 +873,9 @@ zfopendata(char *name)
 	     * Fall back to send port mode.  That will
 	     * test the preferences for whether that's OK.
 	     */
-	    zfstatus |= ZFST_NOPS;
+	    zfstatusp[zfsessno] |= ZFST_NOPS;
 	    zfclosedata();
-	    return zfopendata(name);
+	    return zfopendata(name, zdsockp, is_passivep);
 	}
 	/*
 	 * OK, now we need to know what port we're looking at,
@@ -881,12 +896,13 @@ zfopendata(char *name)
 	iport[0] = STOUC(nums[4]);
 	iport[1] = STOUC(nums[5]);
 
-	memcpy(&zdsock.sin_addr, iaddr, sizeof(iaddr));
-	memcpy(&zdsock.sin_port, iport, sizeof(iport));
+	memcpy(&zdsockp->sin_addr, iaddr, sizeof(iaddr));
+	memcpy(&zdsockp->sin_port, iport, sizeof(iport));
 
 	/* we should timeout this connect */
 	do {
-	    err = connect(zdfd, (struct sockaddr *)&zdsock, sizeof(zdsock));
+	    err = connect(zfsess->dfd,
+			  (struct sockaddr *)zdsockp, sizeof(*zdsockp));
 	} while (err && errno == EINTR && !errflag);
 
 	if (err) {
@@ -895,7 +911,7 @@ zfopendata(char *name)
 	    return 1;
 	}
 
-	zfpassive_conn = 1;
+	*is_passivep = 1;
     } else {
 	char portcmd[40];
 	unsigned char *addr, *port;
@@ -906,14 +922,16 @@ zfopendata(char *name)
 	    return 1;
 	}
 
-	zdsock.sin_port = 0;	/* to be set by bind() */
-	len = sizeof(zdsock);
+	zdsockp->sin_port = 0;	/* to be set by bind() */
+	len = sizeof(*zdsockp);
 	/* need to do timeout stuff and probably handle EINTR here */
-	if (bind(zdfd, (struct sockaddr *)&zdsock, sizeof(zdsock)) < 0)
+	if (bind(zfsess->dfd, (struct sockaddr *)zdsockp,
+		 sizeof(*zdsockp)) < 0)
 	    ret = 1;
-	else if (getsockname(zdfd, (struct sockaddr *)&zdsock, &len) < 0)
+	else if (getsockname(zfsess->dfd, (struct sockaddr *)zdsockp,
+			     &len) < 0)
 	    ret = 2;
-	else if (listen(zdfd, 1) < 0)
+	else if (listen(zfsess->dfd, 1) < 0)
 	    ret = 3;
 	else
 	    ret = 0;
@@ -926,8 +944,8 @@ zfopendata(char *name)
 	    return 1;
 	}
 
-	addr = (unsigned char *) &zdsock.sin_addr;
-	port = (unsigned char *) &zdsock.sin_port;
+	addr = (unsigned char *) &zdsockp->sin_addr;
+	port = (unsigned char *) &zdsockp->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) {
@@ -935,7 +953,7 @@ zfopendata(char *name)
 	    zfclosedata();
 	    return 1;
 	}
-	zfpassive_conn = 0;
+	*is_passivep = 0;
     }
 
     return 0;
@@ -947,15 +965,15 @@ zfopendata(char *name)
 static void
 zfclosedata(void)
 {
-    if (zdfd == -1)
+    if (zfsess->dfd == -1)
 	return;
-    close(zdfd);
-    zdfd = -1;
+    close(zfsess->dfd);
+    zfsess->dfd = -1;
 }
 
 /*
  * Set up a data connection and use cmd to initiate a transfer.
- * The actual data fd will be zdfd; the calling routine
+ * The actual data fd will be zfsess->dfd; 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.
@@ -969,9 +987,10 @@ zfclosedata(void)
 static int
 zfgetdata(char *name, char *rest, char *cmd, int getsize)
 {
-    int len, newfd;
+    int len, newfd, is_passive;
+    struct sockaddr_in zdsock;
 
-    if (zfopendata(name))
+    if (zfopendata(name, &zdsock, &is_passive))
 	return 1;
 
     /*
@@ -993,7 +1012,8 @@ zfgetdata(char *name, char *rest, char *cmd, int getsize)
 	zfclosedata();
 	return 1;
     }
-    if (getsize || (!(zfstatus & ZFST_TRSZ) && !strncmp(cmd, "RETR", 4))) {
+    if (getsize || (!(zfstatusp[zfsessno] & ZFST_TRSZ) &&
+		    !strncmp(cmd, "RETR", 4))) {
 	/*
 	 * See if we got something like:
 	 *   Opening data connection for nortypix.gif (1234567 bytes).
@@ -1001,14 +1021,14 @@ zfgetdata(char *name, char *rest, char *cmd, int getsize)
 	 * can avoid sending a special SIZE command beforehand.
 	 */
 	char *ptr = strstr(lastmsg, "bytes");
-	zfstatus |= ZFST_NOSZ|ZFST_TRSZ;
+	zfstatusp[zfsessno] |= ZFST_NOSZ|ZFST_TRSZ;
 	if (ptr) {
 	    while (ptr > lastmsg && !isdigit(STOUC(*ptr)))
 		ptr--;
 	    while (ptr > lastmsg && isdigit(STOUC(ptr[-1])))
 		ptr--;
 	    if (isdigit(STOUC(*ptr))) {
-		zfstatus &= ~ZFST_NOSZ;
+		zfstatusp[zfsessno] &= ~ZFST_NOSZ;
 		if (getsize) {
 		    off_t sz = zstrtol(ptr, NULL, 10);
 		    zfsetparam("ZFTP_SIZE", &sz, ZFPM_READONLY|ZFPM_INTEGER);
@@ -1017,9 +1037,9 @@ zfgetdata(char *name, char *rest, char *cmd, int getsize)
 	}
     }
 
-    if (!zfpassive_conn) {
+    if (!is_passive) {
 	/*
-	 * the current zdfd is the socket we opened, but we need
+	 * the current zfsess->dfd 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.
@@ -1027,20 +1047,21 @@ zfgetdata(char *name, char *rest, char *cmd, int getsize)
 
 	/* accept the connection */
 	len = sizeof(zdsock);
-	newfd = zfmovefd(accept(zdfd, (struct sockaddr *)&zdsock, &len));
+	newfd = zfmovefd(accept(zfsess->dfd, (struct sockaddr *)&zdsock, 
+				&len));
 	if (newfd < 0)
 	    zwarnnam(name, "unable to accept data: %e", NULL, errno);
 	zfclosedata();
 	if (newfd < 0)
 	    return 1;
-	zdfd = newfd;		/* this is now the actual data fd */
+	zfsess->dfd = newfd;	/* this is now the actual data fd */
     } else {
 	/*
-	 * We avoided dup'ing zdfd up to this point, to try to keep
+	 * We avoided dup'ing zfsess->dfd up to this point, to try to keep
 	 * things simple, so we now need to move it out of the way
 	 * of the user-visible fd's.
 	 */
-	zdfd = zfmovefd(zdfd);
+	zfsess->dfd = zfmovefd(zfsess->dfd);
     }
 
 
@@ -1059,20 +1080,21 @@ zfgetdata(char *name, char *rest, char *cmd, int getsize)
 
 	li.l_onoff = 1;
 	li.l_linger = 120;
-	setsockopt(zdfd, SOL_SOCKET, SO_LINGER, (char *)&li, sizeof(li));
+	setsockopt(zfsess->dfd, 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));
+	setsockopt(zfsess->dfd, 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);
+	fcntl(zfsess->dfd, F_SETFD, &len);
 #endif
 
     return 0;
@@ -1103,15 +1125,15 @@ zfstats(char *fnam, int remote, off_t *retsize, char **retmdtm, int fd)
 	*retmdtm = NULL;
     if (remote) {
 	char *cmd;
-	if ((zfhas_size == ZFCP_NOPE && retsize) ||
-	    (zfhas_mdtm == ZFCP_NOPE && retmdtm))
+	if ((zfsess->has_size == ZFCP_NOPE && retsize) ||
+	    (zfsess->has_mdtm == ZFCP_NOPE && retmdtm))
 	    return 2;
 
 	/*
 	 * File is coming from over there.
 	 * Make sure we get the type right.
 	 */
-	zfsettype(ZFST_TYPE(zfstatus));
+	zfsettype(ZFST_TYPE(zfstatusp[zfsessno]));
 	if (retsize) {
 	    cmd = tricat("SIZE ", fnam, "\r\n");
 	    ret = zfsendcmd(cmd);
@@ -1120,9 +1142,9 @@ zfstats(char *fnam, int remote, off_t *retsize, char **retmdtm, int fd)
 		return 1;
 	    else if (lastcode < 300) {
 		sz = zstrtol(lastmsg, 0, 10);
-		zfhas_size = ZFCP_YUPP;
+		zfsess->has_size = ZFCP_YUPP;
 	    } else if (lastcode >= 500 && lastcode <= 504) {
-		zfhas_size = ZFCP_NOPE;
+		zfsess->has_size = ZFCP_NOPE;
 		return 2;
 	    } else if (lastcode == 550)
 		return 1;
@@ -1137,9 +1159,9 @@ zfstats(char *fnam, int remote, off_t *retsize, char **retmdtm, int fd)
 		return 1;
 	    else if (lastcode < 300) {
 		mt = ztrdup(lastmsg);
-		zfhas_mdtm = ZFCP_YUPP;
+		zfsess->has_mdtm = ZFCP_YUPP;
 	    } else if (lastcode >= 500 && lastcode <= 504) {
-		zfhas_mdtm = ZFCP_NOPE;
+		zfsess->has_mdtm = ZFCP_NOPE;
 		return 2;
 	    } else if (lastcode == 550)
 		return 1;
@@ -1387,20 +1409,20 @@ zfsenddata(char *name, int recv, int progress, off_t startat)
 	sofar = last_sofar = startat;
     }
     if (recv) {
-	fdin = zdfd;
+	fdin = zfsess->dfd;
 	fdout = 1;
 	rtmout = getiparam("ZFTP_TMOUT");
-	if (ZFST_CTYP(zfstatus) == ZFST_ASCI)
+	if (ZFST_CTYP(zfstatusp[zfsessno]) == ZFST_ASCI)
 	    fromasc = 1;
-	if (ZFST_MODE(zfstatus) == ZFST_BLOC)
+	if (ZFST_MODE(zfstatusp[zfsessno]) == ZFST_BLOC)
 	    read_ptr = zfread_block;
     } else {
 	fdin = 0;
-	fdout = zdfd;
+	fdout = zfsess->dfd;
 	wtmout = getiparam("ZFTP_TMOUT");
-	if (ZFST_CTYP(zfstatus) == ZFST_ASCI)
+	if (ZFST_CTYP(zfstatusp[zfsessno]) == ZFST_ASCI)
 	    toasc = 1;
-	if (ZFST_MODE(zfstatus) == ZFST_BLOC)
+	if (ZFST_MODE(zfstatusp[zfsessno]) == ZFST_BLOC)
 	    write_ptr = zfwrite_block;
     }
 
@@ -1525,7 +1547,8 @@ zfsenddata(char *name, int recv, int progress, off_t startat)
      * so we don't need to force the control connection to close.
      */
     zfdrrrring = 0;
-    if (!errflag && !ret && !recv && ZFST_MODE(zfstatus) == ZFST_BLOC) {
+    if (!errflag && !ret && !recv &&
+	ZFST_MODE(zfstatusp[zfsessno]) == ZFST_BLOC) {
 	/* send an end-of-file marker block */
 	ret = (zfwrite_block(fdout, lsbuf, 0, wtmout) < 0);
     }
@@ -1554,8 +1577,8 @@ zfsenddata(char *name, int recv, int progress, off_t startat)
 
 	/* 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);
+	send(zfsess->cfd, (char *)msg, 3, 0);
+	send(zfsess->cfd, (char *)msg+3, 1, MSG_OOB);
 
 	zfsendcmd("ABOR\r\n");
 	if (lastcode == 226) {
@@ -1603,8 +1626,8 @@ zftp_open(char *name, char **args, int flags)
     int err, len, tmout;
 
     if (!*args) {
-	if (zfuserparams)
-	    args = zfuserparams;
+	if (zfsess->userparams)
+	    args = zfsess->userparams;
 	else {
 	    zwarnnam(name, "no host specified", NULL, 0);
 	    return 1;
@@ -1616,8 +1639,8 @@ zftp_open(char *name, char **args, int flags)
      * Probably this is the safest thing to do.  It's possible
      * a `QUIT' will hang, though.
      */
-    if (zcfd != -1)
-	zfclose();
+    if (zfsess->cfd != -1)
+	zfclose(0);
 
     /* this is going to give 0.  why bother? */
     zprotop = getprotobyname("tcp");
@@ -1643,7 +1666,7 @@ zftp_open(char *name, char **args, int flags)
 	    zwarnnam(name, "timeout connecting to %s", hname, 0);
 	else
 	    zwarnnam(name, "timeout on host name lookup", NULL, 0);
-	zfclose();
+	zfclose(0);
 	return 1;
     }
     zfalarm(tmout);
@@ -1666,7 +1689,7 @@ zftp_open(char *name, char **args, int flags)
 	 * or, we could have a `no_lookup' flag.
 	 */
 	zfsetparam("ZFTP_HOST", ztrdup(args[0]), ZFPM_READONLY);
-	zsock.sin_family = AF_INET;
+	zfsess->sock.sin_family = AF_INET;
     } else {
 	zhostp = gethostbyname(args[0]);
 	if (!zhostp || errflag) {
@@ -1678,18 +1701,20 @@ zftp_open(char *name, char **args, int flags)
 	    alarm(0);
 	    return 1;
 	}
-	zsock.sin_family = zhostp->h_addrtype;
+	zfsess->sock.sin_family = zhostp->h_addrtype;
 	zfsetparam("ZFTP_HOST", ztrdup(zhostp->h_name), ZFPM_READONLY);
     }
 
-    zsock.sin_port = zservp->s_port;
-    zcfd = socket(zsock.sin_family, SOCK_STREAM, 0);
-    if (zcfd < 0) {
+    zfsess->sock.sin_port = zservp->s_port;
+    zfsess->cfd = socket(zfsess->sock.sin_family, SOCK_STREAM, 0);
+    if (zfsess->cfd < 0) {
 	zwarnnam(name, "socket failed: %e", NULL, errno);
 	zfunsetparam("ZFTP_HOST");
 	alarm(0);
 	return 1;
     }
+    /* counts as `open' so long as it's not negative */
+    zfnopen++;
 
     /*
      * now connect the socket.  manual pages all say things like `this is all
@@ -1700,16 +1725,18 @@ zftp_open(char *name, char **args, int flags)
 
     if (ipaddr.s_addr != INADDR_NONE) {
 	/* dot address */
-	memcpy(&zsock.sin_addr, &ipaddr, sizeof(ipaddr));
+	memcpy(&zfsess->sock.sin_addr, &ipaddr, sizeof(ipaddr));
 	do {
-	    err = connect(zcfd, (struct sockaddr *)&zsock, sizeof(zsock));
+	    err = connect(zfsess->cfd, (struct sockaddr *)&zfsess->sock,
+			  sizeof(zfsess->sock));
 	} 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);
+	    memcpy(&zfsess->sock.sin_addr, *addrp, zhostp->h_length);
 	    do {
-		err = connect(zcfd, (struct sockaddr *)&zsock, sizeof(zsock));
+		err = connect(zfsess->cfd, (struct sockaddr *)&zfsess->sock,
+			      sizeof(zfsess->sock));
 	    } while (err && errno == EINTR && !errflag);
 	    /* you can check whether it's worth retrying here */
 	}
@@ -1719,10 +1746,11 @@ zftp_open(char *name, char **args, int flags)
 
     if (err < 0) {
 	zwarnnam(name, "connect failed: %e", NULL, errno);
-	zfclose();
+	zfclose(0);
 	return 1;
     }
-    zfsetparam("ZFTP_IP", ztrdup(inet_ntoa(zsock.sin_addr)), ZFPM_READONLY);
+    zfsetparam("ZFTP_IP", ztrdup(inet_ntoa(zfsess->sock.sin_addr)),
+	       ZFPM_READONLY);
     /* now we can talk to the control connection */
     zcfinish = 0;
 
@@ -1731,18 +1759,18 @@ zftp_open(char *name, char **args, int flags)
      * Move the fd out of the user-visible range.  We need to do
      * this after the connect() on some systems.
      */
-    zcfd = zfmovefd(zcfd);
+    zfsess->cfd = zfmovefd(zfsess->cfd);
 
 #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);
+    fcntl(zfsess->cfd, F_SETFD, &len);
 #endif
 
-    len = sizeof(zsock);
-    if (getsockname(zcfd, (struct sockaddr *)&zsock, &len) < 0) {
+    len = sizeof(zfsess->sock);
+    if (getsockname(zfsess->cfd, (struct sockaddr *)&zfsess->sock, &len) < 0) {
 	zwarnnam(name, "getsockname failed: %e", NULL, errno);
-	zfclose();
+	zfclose(0);
 	return 1;
     }
     /* nice to get some options right, ignore if they don't work */
@@ -1752,67 +1780,75 @@ zftp_open(char *name, char **args, int flags)
      * do clever things with SIGURG.
      */
     len = 1;
-    setsockopt(zcfd, SOL_SOCKET, SO_OOBINLINE, (char *)&len, sizeof(len));
+    setsockopt(zfsess->cfd, 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));
+    setsockopt(zfsess->cfd, 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");
+    zfsess->cin = fdopen(zfsess->cfd, "r");
 
-    if (!zcin) {
+    if (!zfsess->cin) {
 	zwarnnam(name, "file handling error", NULL, 0);
-	zfclose();
+	zfclose(0);
 	return 1;
     }
 
 #ifdef _IONBF
-    setvbuf(zcin, NULL, _IONBF, 0);
+    setvbuf(zfsess->cin, NULL, _IONBF, 0);
 #else
-    setlinebuf(zcin);
+    setlinebuf(zfsess->cin);
 #endif
 
     /*
      * now see what the remote server has to say about that.
      */
     if (zfgetmsg() >= 4) {
-	zfclose();
+	zfclose(0);
 	return 1;
     }
 
-    zfhas_size = zfhas_mdtm = ZFCP_UNKN;
-    zdfd = -1;
+    zfsess->has_size = zfsess->has_mdtm = ZFCP_UNKN;
+    zfsess->dfd = -1;
     /* initial status: open, ASCII data, stream mode 'n' stuff */
-    zfstatus = 0;
+    zfstatusp[zfsessno] = 0;
 
-    /* open file for saving the current status */
-    fname = gettempname();
-    zfstatfd = open(fname, O_RDWR|O_CREAT, 0600);
-    DPUTS(zfstatfd == -1, "zfstatfd not created");
+    /*
+     * Open file for saving the current status.
+     * We keep this open at the end of the session because
+     * it is used to store the status for all sessions.
+     * However, it is closed whenever there are no connections open.
+     */
+    if (zfstatfd == -1) {
+	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);
+	/* 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);
+	unlink(fname);
+    }
 
-    if (zcfd == -1) {
+    if (zfsess->cfd == -1) {
 	/* final paranoid check */
 	return 1;
     }
 	
     zfsetparam("ZFTP_MODE", ztrdup("S"), ZFPM_READONLY);
     /* if remaining arguments, use them to log in. */
-    if (zcfd > -1 && *++args)
+    if (zfsess->cfd > -1 && *++args)
 	return zftp_login(name, args, flags);
     /* if something wayward happened, connection was already closed */
-    return zcfd == -1;
+    return zfsess->cfd == -1;
 }
 
 /*
@@ -1894,8 +1930,8 @@ zftp_params(char *name, char **args, int flags)
     int i, j, len;
 
     if (!*args) {
-	if (zfuserparams) {
-	    for (aptr = zfuserparams, i = 0; *aptr; aptr++, i++) {
+	if (zfsess->userparams) {
+	    for (aptr = zfsess->userparams, i = 0; *aptr; aptr++, i++) {
 		if (i == 2) {
 		    len = strlen(*aptr);
 		    for (j = 0; j < len; j++)
@@ -1904,23 +1940,24 @@ zftp_params(char *name, char **args, int flags)
 		} else
 		    fprintf(stdout, "%s\n", *aptr);
 	    }
-	}
-	return 0;
+	    return 0;
+	} else
+	    return 1;
     }
     if (!strcmp(*args, "-")) {
-	if (zfuserparams)
-	    freearray(zfuserparams);
-	zfuserparams = 0;
+	if (zfsess->userparams)
+	    freearray(zfsess->userparams);
+	zfsess->userparams = 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);
+	if (**aptr == '?')
+	    str = zfgetinfo((*aptr)[1] ? (*aptr+1) : prompts[i], i == 2);
 	else
-	    str = *aptr;
+	    str = (**aptr == '\\') ? *aptr+1 : *aptr;
 	newarr[i] = ztrdup(str);
     }
     if (errflag) {
@@ -1930,9 +1967,9 @@ zftp_params(char *name, char **args, int flags)
 	zfree(newarr, len+1);
 	return 1;
     }
-    if (zfuserparams)
-	freearray(zfuserparams);
-    zfuserparams = newarr;
+    if (zfsess->userparams)
+	freearray(zfsess->userparams);
+    zfsess->userparams = newarr;
     return 0;
 }
 
@@ -1946,10 +1983,10 @@ zftp_login(char *name, char **args, int flags)
     char *user, tbuf[2] = "X";
     int stopit;
 
-    if ((zfstatus & ZFST_LOGI) && zfsendcmd("REIN\r\n") >= 4)
+    if ((zfstatusp[zfsessno] & ZFST_LOGI) && zfsendcmd("REIN\r\n") >= 4)
 	return 1;
 
-    zfstatus &= ~ZFST_LOGI;
+    zfstatusp[zfsessno] &= ~ZFST_LOGI;
     if (*args) {
 	user = *args++;
     } else {
@@ -2005,7 +2042,7 @@ zftp_login(char *name, char **args, int flags)
     }
 
     zsfree(ucmd);
-    if (zcfd == -1)
+    if (zfsess->cfd == -1)
 	return 1;
     if (stopit == 2 || (lastcode != 230 && lastcode != 202)) {
 	zwarnnam(name, "login failed", NULL, 0);
@@ -2018,7 +2055,7 @@ zftp_login(char *name, char **args, int flags)
 	    cnt++;
 	zwarnnam(name, "warning: %d comand arguments not used\n", NULL, cnt);
     }
-    zfstatus |= ZFST_LOGI;
+    zfstatusp[zfsessno] |= ZFST_LOGI;
     zfsetparam("ZFTP_USER", ztrdup(user), ZFPM_READONLY);
     if (acct)
 	zfsetparam("ZFTP_ACCOUNT", ztrdup(acct), ZFPM_READONLY);
@@ -2028,7 +2065,7 @@ zftp_login(char *name, char **args, int flags)
      * won't let us do this until we're logged in; it's fairly safe
      * to delay it here for all systems.
      */
-    if (!(zfprefs & ZFPF_DUMB) && !(zfstatus & ZFST_SYST)) {
+    if (!(zfprefs & ZFPF_DUMB) && !(zfstatusp[zfsessno] & ZFST_SYST)) {
 	if (zfsendcmd("SYST\r\n") == 2) {
 	    char *ptr = lastmsg, *eptr, *systype;
 	    for (eptr = ptr; *eptr; eptr++)
@@ -2042,13 +2079,13 @@ zftp_login(char *name, char **args, int flags)
 		 * We could set this based just on the UNIX part,
 		 * but I don't really know the consequences of that.
 		 */
-		zfstatus |= ZFST_IMAG;
+		zfstatusp[zfsessno] |= ZFST_IMAG;
 	    }
 	    zfsetparam("ZFTP_SYSTEM", systype, ZFPM_READONLY);
 	}
-	zfstatus |= ZFST_SYST;
+	zfstatusp[zfsessno] |= ZFST_SYST;
     }
-    tbuf[0] = (ZFST_TYPE(zfstatus) == ZFST_ASCI) ? 'A' : 'I';
+    tbuf[0] = (ZFST_TYPE(zfstatusp[zfsessno]) == ZFST_ASCI) ? 'A' : 'I';
     zfsetparam("ZFTP_TYPE", ztrdup(tbuf), ZFPM_READONLY);
 
     /*
@@ -2084,7 +2121,7 @@ zftp_test(char *name, char **args, int flags)
     struct timeval tv;
 # endif /* HAVE_POLL */
 
-    if (zcfd == -1)
+    if (zfsess->cfd == -1)
 	return 1;
 
 # ifdef HAVE_POLL
@@ -2092,29 +2129,30 @@ zftp_test(char *name, char **args, int flags)
     /* safety first, though I think POLLIN is more common */
 #   define POLLIN POLLNORM
 #  endif /* HAVE_POLL */
-    pfd.fd = zcfd;
+    pfd.fd = zfsess->cfd;
     pfd.events = POLLIN;
     if ((ret = poll(&pfd, 1, 0)) < 0 && errno != EINTR && errno != EAGAIN)
-	zfclose();
+	zfclose(0);
     else if (ret > 0 && pfd.revents) {
 	/* handles 421 (maybe a bit noisily?) */
 	zfgetmsg();
     }
 # else
     FD_ZERO(&f);
-    FD_SET(zcfd, &f);
+    FD_SET(zfsess->cfd, &f);
     tv.tv_sec = 0;
     tv.tv_usec = 0;
-    if ((ret = select(zcfd +1, (SELECT_ARG_2_T) &f, NULL, NULL, &tv)) < 0
+    if ((ret = select(zfsess->cfd +1, (SELECT_ARG_2_T) &f,
+		      NULL, NULL, &tv)) < 0
 	&& errno != EINTR)
-	zfclose();
+	zfclose(0);
     else if (ret > 0) {
 	/* handles 421 */
 	zfgetmsg();
     }
 # endif /* HAVE_POLL */
-    /* if we now have zcfd == -1, then we've just been dumped out. */
-    return (zcfd == -1) ? 2 : 0;
+    /* if we now have zfsess->cfd == -1, then we've just been dumped out. */
+    return (zfsess->cfd == -1) ? 2 : 0;
 #else
     zfwarnnam(name, "not supported on this system.", NULL, 0);
     return 3;
@@ -2229,14 +2267,14 @@ static int
 zfsettype(int type)
 {
     char buf[] = "TYPE X\r\n";
-    if (ZFST_TYPE(type) == ZFST_CTYP(zfstatus))
+    if (ZFST_TYPE(type) == ZFST_CTYP(zfstatusp[zfsessno]))
 	return 0;
     buf[5] = (ZFST_TYPE(type) == ZFST_ASCI) ? 'A' : 'I';
     if (zfsendcmd(buf) > 2)
 	return 1;
-    zfstatus &= ~(ZFST_TMSK << ZFST_TBIT);
+    zfstatusp[zfsessno] &= ~(ZFST_TMSK << ZFST_TBIT);
     /* shift the type left to set the current type bits */;
-    zfstatus |= type << ZFST_TBIT;
+    zfstatusp[zfsessno] |= type << ZFST_TBIT;
     return 0;
 }
 
@@ -2257,7 +2295,8 @@ zftp_type(char *name, char **args, int flags)
 	 * 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');
+	printf("%c\n", (ZFST_TYPE(zfstatusp[zfsessno]) == ZFST_ASCI) ?
+	       'A' : 'I');
 	fflush(stdout);
 	return 0;
     } else {
@@ -2275,8 +2314,8 @@ zftp_type(char *name, char **args, int flags)
 	    nt = 'I';
     }
 
-    zfstatus &= ~ZFST_TMSK;
-    zfstatus |= (nt == 'I') ? ZFST_IMAG : ZFST_ASCI;
+    zfstatusp[zfsessno] &= ~ZFST_TMSK;
+    zfstatusp[zfsessno] |= (nt == 'I') ? ZFST_IMAG : ZFST_ASCI;
     tbuf[0] = nt;
     zfsetparam("ZFTP_TYPE", ztrdup(tbuf), ZFPM_READONLY);
     return 0;
@@ -2290,7 +2329,8 @@ zftp_mode(char *name, char **args, int flags)
     int nt;
 
     if (!(str = *args)) {
-	printf("%c\n", (ZFST_MODE(zfstatus) == ZFST_STRE) ? 'S' : 'B');
+	printf("%c\n", (ZFST_MODE(zfstatusp[zfsessno]) == ZFST_STRE) ?
+	       'S' : 'B');
 	fflush(stdout);
 	return 0;
     }
@@ -2302,8 +2342,8 @@ zftp_mode(char *name, char **args, int flags)
     cmd[5] = (char) nt;
     if (zfsendcmd(cmd) > 2)
 	return 1;
-    zfstatus &= ZFST_MMSK;
-    zfstatus |= (nt == 'S') ? ZFST_STRE : ZFST_BLOC;
+    zfstatusp[zfsessno] &= ZFST_MMSK;
+    zfstatusp[zfsessno] |= (nt == 'S') ? ZFST_STRE : ZFST_BLOC;
     zfsetparam("ZFTP_MODE", ztrdup(str), ZFPM_READONLY);
     return 0;
 }
@@ -2377,7 +2417,7 @@ zftp_getput(char *name, char **args, int flags)
      * somewhere or in the background.  This seems to me a problem.
      */
 
-    zfsettype(ZFST_TYPE(zfstatus));
+    zfsettype(ZFST_TYPE(zfstatusp[zfsessno]));
 
     if (recv)
 	fflush(stdout);		/* since we may be using fd 1 */
@@ -2395,7 +2435,7 @@ zftp_getput(char *name, char **args, int flags)
 	     * of zftp_progress is delayed until zfsenddata().
 	     */
 	    if ((!(zfprefs & ZFPF_DUMB) &&
-		 (zfstatus & (ZFST_NOSZ|ZFST_TRSZ)) != ZFST_TRSZ)
+		 (zfstatusp[zfsessno] & (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);
@@ -2413,12 +2453,19 @@ zftp_getput(char *name, char **args, int flags)
 	}
 
 	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))
+	/* note zfsess->dfd doesn't exist till zfgetdata() creates it */
+	if (zfgetdata(name, rest, ln, getsize))
+	    ret = 2;
+	else if (zfsenddata(name, recv, progress, startat))
 	    ret = 1;
 	zsfree(ln);
-	if (progress && (l = getshfunc("zftp_progress")) != &dummy_list) {
+	/*
+	 * The progress report isn't started till zfsenddata(), where
+	 * it's the first item.  Hence we send a final progress report
+	 * if and only if we called zfsenddata();
+	 */
+	if (progress && ret != 2 &&
+	    (l = getshfunc("zftp_progress")) != &dummy_list) {
 	    /* progress to finish: ZFTP_TRANSFER set to GF or PF */
 	    int osc = sfcontext;
 
@@ -2436,7 +2483,7 @@ zftp_getput(char *name, char **args, int flags)
 	    break;
     }
     zfendtrans();
-    return ret;
+    return ret != 0;
 }
 
 /*
@@ -2514,14 +2561,22 @@ zftp_quote(char *name, char **args, int flags)
     return ret;
 }
 
-/* Close the connection, ending the session */
+/*
+ * Close the connection, ending the session.  With leaveparams,
+ * don't do anything to the external status (parameters, zftp_chpwd),
+ * probably because this isn't the current session.
+ */
 
 /**/
-static int
-zftp_close(char *name, char **args, int flags)
+static void
+zfclose(int leaveparams)
 {
     char **aptr;
     List l;
+
+    if (zfsess->cfd == -1)
+	return;
+
     zfclosing = 1;
     if (zcfinish != 2) {
 	/*
@@ -2531,45 +2586,265 @@ zftp_close(char *name, char **args, int flags)
 	 */
 	zfsendcmd("QUIT\r\n");
     }
-    if (zcin)
-	fclose(zcin);
-    zcin = NULL;
-    close(zcfd);
-    zcfd = -1;
+    if (zfsess->cin) {
+	fclose(zfsess->cin);
+	zfsess->cin = NULL;
+    }
+    if (zfsess->cfd != -1) {
+	zfnopen--;
+	close(zfsess->cfd);
+	zfsess->cfd = -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;
+    if (zfstatfd != -1) {
+	zfstatusp[zfsessno] |= ZFST_CLOS;
+	if (!zfnopen) {
+	    /* Write the final status in case this is a subshell */
+	    lseek(zfstatfd, zfsessno*sizeof(int), 0);
+	    write(zfstatfd, zfstatusp+zfsessno, sizeof(int));
+
+	    close(zfstatfd);
+	    zfstatfd = -1;
+	}
+    }
 
-    /* Unset the non-special parameters */
-    for (aptr = zfparams; *aptr; aptr++)
-	zfunsetparam(*aptr);
+    if (!leaveparams) {
+	/* 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) {
-	int osc = sfcontext;
+	/* Now ZFTP_PWD is unset.  It's up to zftp_chpwd to notice. */
+	if ((l = getshfunc("zftp_chpwd")) != &dummy_list) {
+	    int osc = sfcontext;
 
-	sfcontext = SFC_HOOK;
-	doshfunc("zftp_chpwd", l, NULL, 0, 1);
-	sfcontext = osc;
+	    sfcontext = SFC_HOOK;
+	    doshfunc("zftp_chpwd", l, NULL, 0, 1);
+	    sfcontext = osc;
+	}
     }
+
     /* tidy up status variables, because mess is bad */
     zfclosing = zfdrrrring = 0;
+}
+
+/* Safe front end to zftp_close() from within the package */
 
+/**/
+static int
+zftp_close(char *name, char **args, int flags)
+{
+    zfclose(0);
     return 0;
 }
 
-/* Safe front end to zftp_close() from within the package */
+
+/*
+ * Session management routines.  A session consists of various
+ * internal variables describing the connection, the set of shell
+ * parameters --- the same set which is unset by closing a connection ---
+ * and the set of host/user parameters if set by zftp params.
+ */
+
+/*
+ * Switch to a new session, creating it if necessary.
+ * Sets zfsessno, zfsess and $ZFTP_SESSION; updates zfsesscnt and zfstatusp.
+ */
 
 /**/
 static void
-zfclose(void)
+newsession(char *nm)
 {
-    if (zcfd != -1)
-	zftp_close("zftp close", NULL, 0);
+    LinkNode nptr;
+
+    for (zfsessno = 0, nptr = firstnode(zfsessions);
+	 nptr; zfsessno++, incnode(nptr)) {
+	zfsess = (Zftp_session) nptr->dat;
+	if (!strcmp(zfsess->name, nm))
+	    break;
+    }
+
+    if (!nptr) {
+	PERMALLOC {
+	    zfsess = (Zftp_session) zcalloc(sizeof(struct zftp_session));
+	    zfsess->name = ztrdup(nm);
+	    zfsess->cfd = zfsess->dfd = -1;
+	    zfsess->params = (char **) zcalloc(sizeof(zfparams));
+	    addlinknode(zfsessions, zfsess);
+
+	    zfsesscnt++;
+	    zfstatusp = (int *)zrealloc(zfstatusp, sizeof(int)*zfsesscnt);
+	    zfstatusp[zfsessno] = 0;
+	} LASTALLOC;
+    }
+
+    zfsetparam("ZFTP_SESSION", ztrdup(zfsess->name), ZFPM_READONLY);
+}
+
+/* Save the existing session: this just means saving the parameters. */
+
+static void
+savesession()
+{
+    char **ps, **pd, *val;
+
+    for (ps = zfparams, pd = zfsess->params; *ps; ps++, pd++) {
+	if (*pd)
+	    zsfree(*pd);
+	if ((val = getsparam(*ps)))
+	    *pd = ztrdup(val);
+	else
+	    *pd = NULL;
+    }
+    *pd = NULL;
+}
+
+/*
+ * Switch to session nm, creating it if necessary.
+ * Just call newsession, then set up the session-specific parameters.
+ */
+
+/**/
+static void
+switchsession(char *nm)
+{
+    char **ps, **pd;
+
+    newsession(nm);
+
+    for (ps = zfparams, pd = zfsess->params; *ps; ps++, pd++) {
+	if (*pd) {
+	    /* Use the permanently allocated string for the parameter */
+	    zfsetparam(*ps, *pd, ZFPM_READONLY);
+	    *pd = NULL;
+	} else
+	    zfunsetparam(*ps);
+    }
+}
+
+/**/
+static void
+freesession(Zftp_session sptr)
+{
+    char **ps, **pd;
+    zsfree(sptr->name);
+    for (ps = zfparams, pd = zfsess->params; *ps; ps++, pd++)
+	if (*pd)
+	    zsfree(*pd);
+    zfree(zfsess->params, sizeof(zfparams));
+    if (sptr->userparams)
+	freearray(sptr->userparams);
+    zfree(sptr, sizeof(struct zftp_session));
+}
+
+/**/
+static int
+zftp_session(char *name, char **args, int flags)
+{
+    if (!*args) {
+	LinkNode nptr;
+
+	for (nptr = firstnode(zfsessions); nptr; incnode(nptr))
+	    printf("%s\n", ((Zftp_session)nptr->dat)->name);
+	return 0;
+    }
+
+    /*
+     * Check if we are already in the required session: if so,
+     * it's a no-op, not an error.
+     */
+    if (!strcmp(*args, zfsess->name))
+	return 0;
+
+    savesession();
+    switchsession(*args);
+    return 0;
+}
+
+/* Remove a session and free it */
+
+/**/
+static int
+zftp_rmsession(char *name, char **args, int flags)
+{
+    int no;
+    LinkNode nptr;
+    Zftp_session sptr = NULL;
+    char *newsess = NULL;
+
+    /* Find the session in the list: either the current one, or by name */
+    for (no = 0, nptr = firstnode(zfsessions); nptr; no++, nptr++) {
+	sptr = (Zftp_session) nptr->dat;
+	if ((!*args && sptr == zfsess) ||
+	    (*args && !strcmp(sptr->name, *args)))
+	    break;
+    }
+    if (!nptr)
+	return 1;
+
+    if (sptr == zfsess) {
+	/* Freeing current session: make sure it's closed */
+	zfclosedata();
+	zfclose(0);
+
+	/*
+	 * Choose new session to switch to if any: first in list
+	 * excluding the one just freed.
+	 */
+	if (zfsesscnt > 1) {
+	    LinkNode newn = firstnode(zfsessions);
+	    if (newn == nptr)
+		incnode(newn);
+	    newsess = ((Zftp_session)nptr->dat)->name;
+	}
+    } else {
+	Zftp_session oldsess = zfsess;
+	zfsess = sptr;
+	/*
+	 * Freeing another session: don't need to switch, just
+	 * tell zfclose() not to delete parameters etc.
+	 */
+	zfclosedata();
+	zfclose(1);
+	zfsess = oldsess;
+    }
+    remnode(zfsessions, nptr);
+    freesession(sptr);
+
+    /*
+     * Fix up array of status pointers.
+     */
+    if (--zfsesscnt) {
+	/*
+	 * Some remaining, so just shift up
+	 */
+	int *newstatusp = (int *)zalloc(sizeof(int)*zfsesscnt);
+	int *src, *dst, i;
+	for (i = 0, src = zfstatusp, dst = newstatusp; i < zfsesscnt;
+	     i++, src++, dst++) {
+	    if (i == no)
+		src++;
+	    *dst = *src;
+	}
+	zfree(zfstatusp, sizeof(int)*(zfsesscnt+1));
+	zfstatusp = newstatusp;
+
+	/*
+	 * Maybe we need to switch to one of the remaining sessions.
+	 */
+	if (newsess)
+	    switchsession(newsess);
+    } else {
+	zfree(zfstatusp, sizeof(int));
+	zfstatusp = NULL;
+
+	/*
+	 * We've just deleted the last session, so we need to
+	 * start again from scratch.
+	 */
+	newsession("default");
+    }
+
+    return 0;
 }
 
 /* The builtin command frontend to the rest of the package */
@@ -2601,33 +2876,33 @@ bin_zftp(char *name, char **args, char *ops, int func)
     }
 
     strcat(fullname, cnam);
-    if (zfstatfd != -1) {
+    if (zfstatfd != -1 && !(zptr->flags & ZFTP_SESS)) {
 	/* Get the status in case it was set by a forked process */
-	int oldstatus = zfstatus;
+	int oldstatus = zfstatusp[zfsessno];
 	lseek(zfstatfd, 0, 0);
-	read(zfstatfd, &zfstatus, sizeof(zfstatus));
-	if (zcfd != -1 && (zfstatus & ZFST_CLOS)) {
+	read(zfstatfd, zfstatusp, sizeof(int)*zfsesscnt);
+	if (zfsess->cfd != -1 && (zfstatusp[zfsessno] & ZFST_CLOS)) {
 	    /* got closed in subshell without us knowing */
 	    zcfinish = 2;
-	    zfclose();
+	    zfclose(0);
 	} 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))
+	    if (ZFST_TYPE(oldstatus) != ZFST_TYPE(zfstatusp[zfsessno]))
 		zfsetparam("ZFTP_TYPE",
-			   ztrdup(ZFST_TYPE(zfstatus) == ZFST_ASCI ?
+			   ztrdup(ZFST_TYPE(zfstatusp[zfsessno]) == ZFST_ASCI ?
 				  "A" : "I"), ZFPM_READONLY);
-	    if (ZFST_MODE(oldstatus) != ZFST_MODE(zfstatus))
+	    if (ZFST_MODE(oldstatus) != ZFST_MODE(zfstatusp[zfsessno]))
 		zfsetparam("ZFTP_MODE",
-			   ztrdup(ZFST_MODE(zfstatus) == ZFST_BLOC ?
+			   ztrdup(ZFST_MODE(zfstatusp[zfsessno]) == ZFST_BLOC ?
 				  "B" : "S"), ZFPM_READONLY);
 	}
     }
 #if defined(HAVE_SELECT) || defined (HAVE_POLL)
-    if (zcfd != -1 && !(zptr->flags & ZFTP_TEST)) {
+    if (zfsess->cfd != -1 && !(zptr->flags & (ZFTP_TEST|ZFTP_SESS))) {
 	/*
 	 * Test the connection for a bad fd or incoming message, but
 	 * only if the connection was last heard of open, and
@@ -2637,7 +2912,7 @@ bin_zftp(char *name, char **args, char *ops, int func)
 	ret = zftp_test("zftp test", NULL, 0);
     }
 #endif
-    if ((zptr->flags & ZFTP_CONN) && zcfd == -1) {
+    if ((zptr->flags & ZFTP_CONN) && zfsess->cfd == -1) {
 	if (ret != 2) {
 	    /*
 	     * with ret == 2, we just got dumped out in the test,
@@ -2686,12 +2961,15 @@ bin_zftp(char *name, char **args, char *ops, int func)
     if (zfdrrrring) {
 	/* had a timeout, close the connection */
 	zcfinish = 2;		/* don't try sending QUIT */
-	zfclose();
+	zfclose(0);
     }
     if (zfstatfd != -1) {
-	/* Set the status in case another process needs to know */
-	lseek(zfstatfd, 0, 0);
-	write(zfstatfd, &zfstatus, sizeof(zfstatus));
+	/*
+	 * Set the status in case another process needs to know,
+	 * but only for the active session.
+	 */
+	lseek(zfstatfd, zfsessno*sizeof(int), 0);
+	write(zfstatfd, zfstatusp+zfsessno, sizeof(int));
     }
     return ret;
 }
@@ -2719,7 +2997,13 @@ boot_zftp(Module m)
 	zfsetparam("ZFTP_PREFS", ztrdup("PS"), ZFPM_IFUNSET);
 	/* default preferences if user deletes variable */
 	zfprefs = ZFPF_SNDP|ZFPF_PASV;
+    
+	PERMALLOC {
+	    zfsessions = newlinklist();
+	} LASTALLOC;
+	newsession("default");
     }
+
     return !ret;
 }
 
@@ -2733,11 +3017,22 @@ cleanup_zftp(Module m)
      * There are various parameters hanging around, but they're
      * all non-special so are entirely non-life-threatening.
      */
-    zfclosedata();
-    zfclose();
+    LinkNode nptr;
+    Zftp_session cursess = zfsess;
+    for (zfsessno = 0, nptr = firstnode(zfsessions); nptr;
+	 zfsessno++, incnode(nptr)) {
+	zfsess = (Zftp_session)nptr->dat;
+	zfclosedata();
+	/*
+	 * When closing the current session, do the usual unsetting,
+	 * otherwise don't.
+	 */
+	zfclose(zfsess != cursess);
+    }
     zsfree(lastmsg);
-    if (zfuserparams)
-	freearray(zfuserparams);
+    zfunsetparam("ZFTP_SESSION");
+    freelinklist(zfsessions, (FreeFunc) freesession);
+    zfree(zfstatusp, sizeof(int)*zfsesscnt);
     deletebuiltins(m->nam, bintab, sizeof(bintab)/sizeof(*bintab));
     return 0;
 }