# zftp is a loadable module implementing an FTP client as a builtin
# command so that you can use the shell command language and line
# editing to make life easier.  If your system has dynamically
# load libraries and zsh was compiled to use them, it is probably
# somewhere where it can be loaded at run time.  Otherwise, it depends
# whether the shell was compiled with zftp already built into it.
#
# Here is a suite of functions, plus assorted other code, to make
# zftp work smoothly.
#
# Completion is implemented in a fairly natural way, except that
# very little support has been provided for non-UNIX remote hosts.
# On such machines, the safest thing to do is only try to complete
# files in the current directory; this should be OK.
#
# Remote globbing for commands which retrieve files is also
# implemented.  This can be done in two different ways.  The default
# is for zsh to do the globbing locally.  The advantage is that full
# zsh pattern matching (respecting the setting of extendedglob) is
# possible, and no assumption (apart from the restrictions on
# directory handling noted above) is made about the behaviour of the
# server.  The disadvantage is that the entire filename list for the
# current directory must be retrieved, and then zsh must laboriously
# do pattern matching against every file, so it is potentially slow
# for large directories.  Only the non-directory part of file names is
# globbed.
#
# The alternative will be used if $zfrglob has non-zero length.
# Zsh then sends the pattern to the server for globbing.  Best of
# luck.
#
# To support remote globbing, some functions have been aliased
# with 'noglob' in front.  Currently, this has a dire effect on
# completion unless the completeinaliases option is set, so
# it is set below.  This can conceivably cause you problems
# if you expect completion for aliases automatically to give you
# completion for the base command.  I suspect that most people
# don't even know that happens.
#
# The following functions are provided.
#
# General status changing and displaying functions:
# zfparams
#   Simple front end to `zftp params', except it will automatically
#   query host, user and password.  These are then stored to be
#   used with a `zfopen' with no arguments.
# zfopen [ host [ user ... ] ]
#   Open a connection and login.  Unless the option -1 (once)
#   is given, will store the parameters for the open (including
#   a password which is prompted for and not echoed) so that
#   if you call zfopen subsequently without arguments it will
#   reopen the same connection.
# zfanon anonftphost
#   Open a connection for anonymous FTP.  Tries to guess an
#   email address to use as the password, unless $EMAIL_ADDR is
#   already set.  The first time, will tell you what it has guessed.
#   It's rude to set EMAIL_ADDR=mozilla.
# zfcd [ dir | old new ]
#   Change directory on the server.  This tries to mimic the behaviour
#   of the shell's cd.  In particular,
#    zfcd           change to '~' on server, if it interprets it
#    zfcd -         change to previous directory of current connection
#    zfcd OLD NEW   change directory from fooOLDbar to fooNEWbar
#   One piece of magic is builtin:  an initial part of the directory
#   matching $HOME is translated back to `~'.  Most UNIX servers
#   recognise the usual shell convention.  So things like `zfcd $PWD'
#   is useful provide you are under your home directory and the
#   structure on the remote machine mirrors that on the local.
# zfhere
#   Synonym for `zfcd $PWD', see above.
# zfdir [args]
#   Show a long diretory list of the remote connection.  Any
#   arguments are passed on to the server, apart from options.
#   Currently this always uses a pager to show the directory
#   list.  Caching is implemented:  zfdir on its own always shows
#   the current diretory, which is cached; zfdir with some other
#   directory arguments shows that, which is cached separately
#   and can be reviewed with `zfdir -r'.  Other options:
#    -f  force reget, overriding the cache, in case something's changed
#    -d  delete the cache, but don't show anything.
#   To pass options to the server, use e.g. `zfdir -- -C'.
#   This also has the zfcd ~ hack.
# zfls [args]
#   Short list of the long directory, depending on what [args]
#   do to the server.  No options, no caching, no pager.
# zftype [ a[scii] | i[mage] | b[inary] ]
#   Set or display the transfer type; currently only ASCII
#   and image (same as binary) types are supported.
# zfclose
#   Close the connection.
# zfstat
#   Print the zftp status from local variables; doesn't do any network
#   operations unless -v is supplied, in which case the server is
#   asked for its views on the status, too.
#
# Functions for retrieving data:
#   All accept the following options:
#    -G   Don't do remote globbing (see above); the default is to do it.
#    -t   Try to set local files to the same time as the remote ones.
#         Unfortunately we only know the remote time in GMT, so it's
#         a little tricky and you need perl 5 (installed as `perl')
#         for this to work.  Suggestions welcome.
# zfget file1 file2 ...
#   Retrieve each file from the server.  The remote file is the
#   full name given, the local file is the non-directory part of that
#   (assuming UNIX file paths).
# zfuget file1 file2 ..
#   Get with update.  Check remote and local sizes and times and
#   retrieve files which are newer on the server.  Will query
#   hard cases, which are where the remote file is newer but a
#   different size, or is older but the same size.  With option -s
#   (silent) assumes it's best to retrieve the files in both those
#   cases.  With -v (may be combined with -s), print the information
#   about the files being considered.
# zfcget file1 ...
#   Assuming file1 was incompletely retrieved, try to get the rest of
#   it.  This relies on a normal UNIX server behaviour which is not
#   as specified in the FTP standard and hence is not universal.
# zfgcp file1 file2
# zfgcp file1 file2 ... dir
#   Get with the behaviour of cp, i.e. copy remote file1 to local
#   file2, or get remote fileN into local diretory dir.
#
# Function for sending data:
# zfput file1 file2 ...
#   Put the local files onto the server under the same name.  The
#   local files are exactly as given; the remote files are the
#   non-diretory parts of that. 
# zfuput file1 file2 ..
#   Put the local files onto the server, with update.  Works
#   similarly to zfuget.
#
# Utility functions:
# zftp_chpwd
#   Show the new directory when it changes; try to put it into
#   an xterm on shelltool header.  Works best alongside chpwd.
# zftp_progress
#   Show the percentage of a file retrieved as it is coming; if the
#   size is not available show the size transferred so far.  The
#   percentage may be wrong if sending data from a local pipe.
#   If you transfer files in the background, you should undefine
#   this before the transfer.  It is smart enough not to print
#   anything when stderr is not a terminal.
# zfcd_match
#   Function for remote directory completion.
# zfget_match
#   Function for remote filename completion.
# zfrglob varname
#   This is used for the remote globbing.  The pattern resides
#   in $varname (note extra level of indirection), and on return
#   $varname will contain the list of matching files.
# zfrtime locfile remfile [ time ]
#   This sad thing does the setting of local file times to those
#   of the remote, see horror story above.

zmodload -ia zftp

alias zfcd='noglob zfcd'
alias zfget='noglob zfget'
alias zfls='noglob zfls'
alias zfdir='noglob zfdir'
alias zfuget='noglob zfuget'
# only way of getting that noglob out of the way at the moment
setopt completealiases

#
# zftp completions: only use these if new-style completion is not
# active.
#
if [[ ${#patcomps} -eq 0 || ${patcomps[(i)zf*]} -gt ${#patcomps} ]]; then
  compctl -f -x 'p[1]' \
    -k '(open params user login type ascii binary mode put putat
    get getat append appendat ls dir local remote mkdir rmdir delete
    close quit)'  - \
    'w[1,cd][1,ls][1,dir][1,rmdir]' -K zfcd_match -S/ -q - \
    'W[1,get*]' -K zfget_match - 'w[1,delete][1,remote]' -K zfget_match - \
    'w[1,open][1,params]' -k hosts -- zftp
  compctl -K zfcd_match -S/ -q zfcd zfdir zfls
  compctl -K zfget_match zfget zfgcp zfuget zfcget
  compctl -k hosts zfanon zfopen zfparams
fi

function zfanon {
  local opt optlist once
  
  while [[ $1 = -* ]]; do
    if [[ $1 = - || $1 = -- ]]; then
      shift;
      break;
    fi
    optlist=${1#-}
    for (( i = 1; i <= $#optlist; i++)); do
      opt=$optlist[$i]
      case $optlist[$i] in
        1) once=1
  	 ;;
        *) print option $opt not recognised >&2
  	 ;;
      esac
    done
    shift
  done
  
  if [[ -z $EMAIL_ADDR ]]; then
    # Exercise in futility.  There's a poem by Wallace Stevens
    # called something like `N ways of looking at a blackbird',
    # where N is somewhere around 0x14 to 0x18.  Now zftp is
    # ashamed to prsent `N ways of looking at a hostname'.
    local domain host
    # First, maybe we've already got it.  Zen-like.
    if [[ $HOST = *.* ]]; then
      # assume this is the full host name
      host=$HOST
    elif [[ -f /etc/resolv.conf ]]; then
      # Next, maybe we've got resolv.conf.
      domain=$(awk '/domain/ { print $2 }' /etc/resolv.conf)
      [[ -n $domain ]] && host=$HOST.$domain
    fi
    # Next, maybe we've got nlsookup.  May not work on LINUX.
    [[ -z $host ]] && host=$(nslookup $HOST | awk '/Name:/ { print $2 }')
    if [[ -z $host ]]; then
      # we're running out of ideas, but this should work.
      # after all, i wrote it...
      # don't want user to know about this, too embarrassed.
      local oldvb=$ZFTP_VERBOSE oldtm=$ZFTP_TMOUT
      ZFTP_VERBOSE=
      ZFTP_TMOUT=5
      if zftp open $host >& /dev/null; then
        host=$ZFTP_HOST
        zftp close $host
      fi
      ZFTP_VERBOSE=$oldvb
      ZFTP_TMOUT=$oldtm
    fi
    if [[ -z $host ]]; then
      print "Can't get your hostname.  Define \$EMAIL_ADDR by hand."
      return 1;
    fi
    EMAIL_ADDR="$USER@$host"
    print "Using $EMAIL_ADDR as anonymous FTP password."
  fi
  
  if [[ $once = 1 ]]; then
    zftp open $1 anonymous $EMAIL_ADDR
  else
    zftp params $1 anonymous $EMAIL_ADDR
    zftp open
  fi
}

function zfautocheck {
  # This function is used to implement auto-open behaviour.
  #
  # With first argument including n, don't change to the old directory; else do.
  #
  # Set do_close to 1 if the connection was not previously open, 0 otherwise
  # With first arguemnt including d, don't set do_close to 1.  Broadly
  # speaking, we use this mechanism to shut the connection after use
  # if the connection had been explicitly closed (i.e. didn't time out,
  # which zftp test investigates) and we are not using a directory
  # command, which implies we are looking for something so should stay open
  # for it.
  
  # Remember the old session:  zflastsession will be overwritten by
  # a successful open.
  local lastsession=$zflastsession
  
  if [[ -z $ZFTP_HOST ]]; then
    zfopen || return 1
    [[ $1 = *d* ]] || do_close=1
  elif zftp test 2>/dev/null; then
    return 0
  else
    zfopen || return 1
  fi
  
  if [[ $1 = *n* ]]; then
    return 0
  else
    zfcd ${lastsession#*:}
  fi
  
}

function zfcd {
  # zfcd:  change directory on the remote server.
  #
  #  Currently has the following features:
  # --- an initial string matching $HOME in the directory is turned back into ~
  #     to be re-interpreted by the remote server.
  # --- zfcd with no arguments changes directory to '~'
  # --- `zfcd old new' and `zfcd -' work analagously to cd
  # --- if the connection is not currently open, it will try to
  #     re-open it with the stored parameters as set by zfopen.
  #     If the connection timed out, however, it won't know until
  #     too late.  In that case, just try the same zfcd command again
  #     (but now `zfcd -' and `zfcd old new' won't work).
  
  # hack: if directory begins with $HOME, turn it back into ~
  # there are two reasons for this:
  #   first, a ~ on the command line gets expanded even with noglob.
  #     (I suppose this is correct, but I wouldn't like to swear to it.)
  #   second, we can no do 'zfcd $PWD' and the like, and that will
  #     work just as long as the directory structures under the home match.
  
  if [[ $1 = /* ]]; then
    zfautocheck -dn
  else
    zfautocheck -d
  fi
  
  if [[ $1 = $HOME || $1 = $HOME/* ]]; then
    1="~${1#$HOME}"
  fi
  
  if (( $# == 0 )); then
    # Emulate `cd' behaviour
    set -- '~'
  elif [[ $# -eq 1 && $1 = - ]]; then
    # Emulate `cd -' behaviour.
    set -- $zflastdir
  elif [[ $# -eq 2 ]]; then
    # Emulate `cd old new' behaviour.
    # We have to find a character not in $1 or $2; ! is a good bet.
    eval set -- "\${ZFTP_PWD:s!$1!$2!}"
  fi
  
  # We have to remember the current directory before changing it
  # if we want to keep it.
  local lastdir=$ZFTP_PWD
  
  zftp cd "$@"  &&  zflastdir=$lastdir
  print $zflastsession
}

function zfcd_match {
  # see zfcd for details of this hack
  if [[ $1 = $HOME || $1 = $HOME/* ]]; then
    1="~${1#$HOME}"
  fi
  
  # error messages only
  local ZFTP_VERBOSE=45
  # should we redirect 2>/dev/null or let the user see it?
  
  local tmpf=${TMPPREFIX}zfcm$$
  
  if [[ $ZFTP_SYSTEM = UNIX* ]]; then
    # hoo, aren't we lucky: this makes things so much easier
    setopt localoptions rcexpandparam
    local dir
    if [[ $1 = ?*/* ]]; then
      dir=${1%/*}
    elif [[ $1 = /* ]]; then
      dir=/
    fi
    # If we're using -F, we get away with using a directory
    # to list, but not a glob.  Don't ask me why.
    # I hate having to rely on awk here.
    zftp ls -F $dir >$tmpf
    reply=($(awk '/\/$/ { print substr($1, 0, length($1)-1) }' $tmpf))
    rm -f $tmpf
    if [[ $dir = / ]]; then
      reply=(${dir}$reply)
    elif [[ -n $dir ]]; then
      reply=($dir/$reply)
    fi
  else
    # I simply don't know what to do here.
    # Just use the list of files for the current directory.
    zfget_match $*
  fi
  
}

function zfcget {
  # Continuation get of files from remote server.
  # For each file, if it's shorter here, try to get the remainder from
  # over there.  This requires the server to support the REST command
  # in the way many do but RFC959 doesn't specify.
  # Options:
  #   -G   don't to remote globbing, else do
  #   -t   update the local file times to the same time as the remote.
  #        Currently this only works if you have the `perl' command,
  #        and that perl is version 5 with the standard library.
  #        See the function zfrtime for more gory details.
  
  setopt localoptions
  unsetopt ksharrays shwordsplit
  
  local loc rem stat=0 optlist opt nglob remlist locst remst
  local tmpfile=${TMPPREFIX}zfcget$$ rstat tsize time
  
  while [[ $1 = -* ]]; do
    if [[ $1 = - || $1 = -- ]]; then
      shift;
      break;
    fi
    optlist=${1#-}
    for (( i = 1; i <= $#optlist; i++)); do
      opt=$optlist[$i]
      case $optlist[$i] in
        G) nglob=1
  	 ;;
        t) time=1
  	 ;;
        *) print option $opt not recognised >&2
  	 ;;
      esac
    done
    shift
  done
  
  for remlist in $*; do
    # zfcd directory hack to put the front back to ~
    if [[ $remlist = $HOME || $remlist = $HOME/* ]]; then
      remlist="~${remlist#$HOME}"
    fi
    if [[ $nglob != 1 ]]; then
      zfrglob remlist
    fi
    if (( $#remlist )); then
      for rem in $remlist; do
        loc=${rem:t}
        if [[ ! -f $loc ]]; then
  	# File does not yet exist
  	zftp get $rem >$loc || stat=$?
        else
  	# Compare the sizes.
  	locst=($(zftp local $loc))
  	zftp remote $rem >$tmpfile
  	rstat=$?
  	remst=($(<$tmpfile))
  	rm -f $tmpfile
  	if [[ $rstat = 2 ]]; then
  	  print "Server does not support SIZE command.\n" \
  	  "Assuming you know what you're doing..." 2>&1
  	  zftp getat $rem $locst[1] >>$loc || stat=$?
  	  continue
  	elif [[ $rstat = 1 ]]; then
  	  print "Remote file not found: $rem" 2>&1
  	  continue
  	fi
  	if [[ $locst[1] -gt $remst[1] ]]; then
  	  print "Local file is larger!" 2>&1
  	  continue;
  	elif [[ $locst[1] == $remst[1] ]]; then
  	  print "Files are already the same size." 2>&1
  	  continue
  	else
  	  if zftp getat $rem $locst[1] >>$loc; then
  	    [[ $time = 1 ]] && zfrtime $loc $rem $remst[2]
  	  else
  	    stat=1
  	  fi
  	fi
        fi
      done
    fi
  done
  
  return $stat
}

function zfclose {
  zftp close
}

function zfcput {
  # Continuation put of files from remote server.
  # For each file, if it's shorter over there, put the remainder from
  # over here.  This uses append, which is standard, so unlike zfcget it's
  # expected to work on any reasonable server... err, as long as it
  # supports SIZE and MDTM.  (It could be enhanced so you can enter the
  # size so far by hand.)  You should probably be in binary transfer
  # mode, thought it's not enforced.
  #
  # To read from midway through a local file, `tail +<n>c' is used.
  # It would be nice to find a way of doing this which works on all OS's.
  
  setopt localoptions
  unsetopt ksharrays shwordsplit
  
  local loc rem stat=0 locst remst offs tailtype
  local tmpfile=${TMPPREFIX}zfcget$$ rstat
  
  # find how tail works.  this is intensely annoying, since it's completely
  # standard in C.  od's no use, since we can only skip whole blocks.
  if [[ $(echo abcd | tail +2c) = bcd ]]; then
    tailtype=c
  elif [[ $(echo abcd | tail --bytes=+2) = bcd ]]; then
    tailtype=b
  else
    print "I can't get your \`tail' to start from from arbitrary characters.\n" \
    "If you know how to do this, let me know." 2>&1
    return 1
  fi
  
  for loc in $*; do
    # zfcd directory hack to put the front back to ~
    rem=$loc
    if [[ $rem = $HOME || $rem = $HOME/* ]]; then
      rem="~${rem#$HOME}"
    fi
    if [[ ! -r $loc ]]; then
      print "Can't read file $loc"
      stat=1
    else
      # Compare the sizes.
      locst=($(zftp local $loc))
      zftp remote $rem >$tmpfile
      rstat=$?
      remst=($(<$tmpfile))
      rm -f $tmpfile
      if [[ $rstat = 2 ]]; then
        print "Server does not support remote status commands.\n" \
        "You will have to find out the size by hand and use zftp append." 2>&1
        stat=1
        continue
      elif [[ $rstat = 1 ]]; then
        # Not found, so just do a standard put.
        zftp put $rem <$loc
      elif [[ $remst[1] -gt $locst[1] ]]; then
        print "Remote file is larger!" 2>&1
        continue;
      elif [[ $locst[1] == $remst[1] ]]; then
        print "Files are already the same size." 2>&1
        continue
      else
        # tail +<N>c takes the count of the character
        # to start from, not the offset from zero. if we did
        # this with years, then 2000 would be 1999.  no y2k bug!
        # brilliant.
        (( offs = $remst[1] + 1 ))
        if [[ $tailtype = c ]]; then
  	tail +${offs}c $loc | zftp append $rem || stat=1
        else
  	tail --bytes=+$offs $loc | zftp append $rem || stat=1
        fi
      fi
    fi
  done
  
  return $stat
}

function zfdir {
  # Long directory of remote server.
  # The remote directory is cached.  In fact, two caches are kept:
  # one of the standard listing of the current directory, i.e. zfdir
  # with no arguments, and another for everything else.
  # To access the appropriate cache, just use zfdir with the same
  # arguments as previously.  zfdir -r will also re-use the `everything
  # else' cache; you can always reuse the current directory cache just
  # with zfdir on its own.
  #
  # The current directory cache is emptied when the directory changes;
  # the other is kept until a new zfdir with a non-empty argument list.
  # Both are removed when the connection is closed.
  #
  # zfdir -f will force the existing cache to be ignored, e.g. if you know
  #          or suspect the directory has changed.
  # zfdir -d will remove both caches without listing anything.
  # If you need to pass -r, -f or -d to the dir itself, use zfdir -- -d etc.;
  # unrecognised options are passed through to dir, but zfdir options must
  # appear first and unmixed with the others.
  
  setopt localoptions unset extendedglob
  unsetopt shwordsplit ksharrays
  
  local file opt optlist redir i newargs force
  
  while [[ $1 = -* ]]; do
    if [[ $1 = - || $1 = -- ]]; then
      shift;
      break;
    elif [[ $1 != -[rfd]## ]]; then
      # pass options through to ls
      break;
    fi
    optlist=${1#-}
    for (( i = 1; i <= $#optlist; i++)); do
      opt=$optlist[$i]
      case $optlist[$i] in
        r) redir=1
  	 ;;
        f) force=1
  	 ;;
        d) [[ -n $zfcurdir && -f $zfcurdir ]] && rm -f $zfcurdir
  	 [[ -n $zfotherdir && -f $zfotherdir ]] && rm -f $zfotherdir
  	 zftp_fcache=()
  	 return 0
  	 ;;
      esac
    done
    shift
  done
  
  zfautocheck -d
  
  # directory hack, see zfcd
  for (( i = 1; i <= $#argv; i++ )); do
    if [[ $argv[$i] = $HOME || $argv[$i] = $HOME/* ]]; then
      argv[$i]="~${argv[$i]#$HOME}"
    fi
  done
  
  if [[ $# -eq 0 ]]; then
    # Cache it in the current directory file.  This means that repeated
    # calls to zfdir with no arguments always use a cached file.
    [[ -z $zfcurdir ]] && zfcurdir=${TMPPREFIX}zfcurdir$$
    file=$zfcurdir
  else
    # Last directly looked at was not the current one, or at least
    # had non-standard arguments.
    [[ -z $zfotherdir ]] && zfotherdir=${TMPPREFIX}zfotherdir$$
    file=$zfotherdir
    newargs="$*"
    if [[ -f $file && $redir != 1 && $force -ne 1 ]]; then
      # Don't use the cached file if the arguments changed.
      [[ $newargs = $zfotherargs ]] || rm -f $file
    fi
    zfotherargs=$newargs
  fi
  
  if [[ $force -eq 1 ]]; then
    rm -f $file
    # if it looks like current directory has changed, better invalidate
    # the filename cache, too.
    (( $# == 0 )) && zftp_fcache=()
  fi
  
  if [[ -n $file && -f $file ]]; then
    eval ${PAGER:-more} \$file
  else
    if (zftp test); then
      # Works OK in subshells
      zftp dir $* | tee $file | eval ${PAGER-:more}
    else
      # Doesn't work in subshells (IRIX 6.2 --- why?)
      zftp dir $* >$file
      eval ${PAGER-:more} >$file
    fi
  fi
}

function zfgcp {
  # ZFTP get as copy:  i.e. first arguments are remote, last is local.
  # Supposed to work exactly like a normal copy otherwise, i.e.
  #  zfgcp rfile lfile
  # or
  #  zfgcp rfile1 rfile2 rfile3 ... ldir
  # Options:
  #   -G   don't to remote globbing, else do
  #   -t   update the local file times to the same time as the remote.
  #        Currently this only works if you have the `perl' command,
  #        and that perl is version 5 with the standard library.
  #        See the function zfrtime for more gory details.
  #
  # If there is no current connection, try to use the existing set of open
  # parameters to establish one and close it immediately afterwards.
  
  setopt localoptions
  unsetopt shwordsplit
  
  local opt optlist nglob remlist rem loc time
  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
  done
  
  zfautocheck
  
  # hmm, we should really check this after expanding the glob,
  # but we shouldn't expand the last argument remotely anyway.
  if [[ $# -gt 2 && ! -d $argv[-1] ]]; then
    print "zfgcp:  last argument must be a directory." 2>&1
    return 1
  elif [[ $# == 1 ]]; then
    print "zfgcp:  not enough arguments." 2>&1
    return 1
  fi
  
  if [[ -d $argv[-1] ]]; then
    local dir=$argv[-1]
    argv[-1]=
    for remlist in $*; do
      # zfcd directory hack to put the front back to ~
      if [[ $remlist = $HOME || $remlist = $HOME/* ]]; then
        remlist="~${remlist#$HOME}"
      fi
      if [[ $nglob != 1 ]]; then
        zfrglob remlist
      fi
      if (( $#remlist )); then
        for rem in $remlist; do
  	loc=$dir/${rem:t}
  	if zftp get $rem >$loc; then
  	  [[ $time = 1 ]] && zfrtime $rem $loc
  	else
  	  stat=1
  	fi
        done
      fi
    done
  else
    zftp get $1 >$2 || stat=$?
  fi
  
  (( $do_close )) && zfclose
  
  return $stat
}

function zfget {
  # Get files from remote server.  Options:
  #   -G   don't to remote globbing, else do
  #   -t   update the local file times to the same time as the remote.
  #        Currently this only works if you have the `perl' command,
  #        and that perl is version 5 with the standard library.
  #        See the function zfrtime for more gory details.
  #
  # If the connection is not currently open, try to open it with the current
  # parameters (set by a previous zfopen or zfparams), then close it after
  # use.  The file is put in the current directory (i.e. using the basename
  # of the remote file only); for more control, use zfgcp.
  
  local loc rem optlist opt nglob remlist time
  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
  done
  
  zfautocheck
  
  for remlist in $*; do
    # zfcd directory hack to put the front back to ~
    if [[ $remlist == $HOME || $remlist == $HOME/* ]]; then
      remlist="~${remlist#$HOME}"
    fi
    if [[ $nglob != 1 ]]; then
      zfrglob remlist
    fi
    if (( $#remlist )); then
      for rem in $remlist; do
        loc=${rem:t}
        if zftp get $rem >$loc; then
  	[[ $time = 1 ]] && zfrtime $rem $loc
        else
  	stat=1
        fi
      done
    fi
  done
  
  (( $do_close )) && zfclose
  
  return $stat
}

function zfget_match {
  # the zfcd hack:  this may not be necessary here
  if [[ $1 == $HOME || $1 == $HOME/* ]]; then
    1="~${1#$HOME}"
  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))
      rm -f $tmpf
    fi
    reply=($zftp_fcache);
  fi
}

function zfhere {
  # Change to the directory corresponding to $PWD on the server.
  # See zfcd for how this works.
  zfcd $PWD
}

function zfls {
  # directory hack, see zfcd
  if [[ $1 = $HOME || $1 = $HOME/* ]]; then
    1="~${1#$HOME}"
  fi
  
  zfautocheck -d
  
  zftp ls $*
}

function zfopen {
  # Use zftp params to set parameters for open, rather than sending
  # them straight to open.  That way they are stored for a future open
  # command.
  #
  # With option -1 (just this 1ce), don't do that.
  
  local optlist opt once
  
  while [[ $1 = -* ]]; do
    if [[ $1 = - || $1 = -- ]]; then
      shift;
      break;
    fi
    optlist=${1#-}
    for (( i = 1; i <= $#optlist; i++)); do
      opt=$optlist[$i]
      case $optlist[$i] in
        1) once=1
  	 ;;
        *) print option $opt not recognised >&2
  	 ;;
      esac
    done
    shift
  done
  
  # This is where we should try and do same name-lookupage in
  # both .netrc and .ncftp/bookmarks .  We could even try saving
  # the info in their for new hosts, like ncftp does.
  
  if [[ $once = 1 ]]; then
    zftp open $*
  else
    # set parameters, but only if there was at least a host
    (( $# > 0 )) && zfparams $*
    # now call with no parameters
    zftp open
  fi
}

function zfparams {
    # Set to prompt for any user or password if not given.
    # Don't worry about accounts here.
    if (( $# > 0 )); then
      (( $# < 2 )) && 2='?'
      (( $# < 3 )) && 3='?'
    fi
    zftp params $*
}

function zfpcp {
  # ZFTP put as copy:  i.e. first arguments are remote, last is local.
  # Currently only supports
  #  zfcp lfile rfile
  # if and only if there are two arguments
  # or
  #  zfcp lfile1 lfile2 lfile3 ... rdir
  # if and only if there are more than two (because otherwise it doesn't
  # know if the last argument is a directory on the remote machine).
  # argument.
  
  setopt localoptions
  unsetopt shwordsplit
  
  local rem loc
  integer stat do_close
  
  zfautocheck
  
  if (( $# > 2 )); then
    local dir=$argv[-1]
    argv[-1]=
    # zfcd directory hack to put the front back to ~
    if [[ $dir = $HOME || $dir = $HOME/* ]]; then
      dir="~${dir#$HOME}"
    fi
    for loc in $*; do
      rem=$dir/${loc:t}
      zftp put $rem <$loc || stat=1
    done
  else
    zftp put $2 <$1 || stat=$?
  fi
  
  (( $do_close )) && zfclose
  
  return $stat
}

function zfput {
  # Simple put:  dump every file under the same name, but stripping
  # off any directory parts.
  
  local loc rem
  integer stat do_close
  
  zfautocheck
  
  for loc in $*; do
    rem=${loc:t}
    zftp put $rem <$loc
    [[ $? == 0 ]] || stat=$?
  done
  
  (( $do_close )) && zfclose
  
  return $stat
}

function zfrglob {
  # Do the remote globbing for zfput, etc.
  # We have two choices:
  #  (1) Get the entire file list and match it one by one
  #      locally against the pattern.
  #      Causes problems if we are globbing directories (rare, presumably).
  #      But: we can cache the current directory, which
  #      we need for completion anyway.  Works on any OS if you
  #      stick with a single directory.  This is the default.
  #  (2) Use remote globbing, i.e. pass it to ls at the site.
  #      Faster, but only works with UNIX, and only basic globbing.
  #      We do this if $zfrglob is non-null.
  
  # There is only one argument, the variable containing the
  # pattern to be globbed.  We set this back to an array containing
  # all the matches.
  setopt localoptions unset
  unsetopt ksharrays
  
  local pat dir nondir files i
  
  eval pat=\$$1
  
  # Check if we really need to do anything.  Look for standard
  # globbing characters, and if extendedglob is set and we are
  # using zsh for the actual pattern matching also look for
  # extendedglob characters.
  if [[ $pat != *[][*?]* &&
    ( -n $zfrglob || ! -o extendedglob || $pat != *[(|)#^]* ) ]]; then
    return 0
  fi
  local tmpf={$TMPPREFIX}zfrglob$$
  
  if [[ $zfrglob != '' ]]; then
    zftp ls "$pat" >$tmpf 2>/dev/null
    eval "$1=(\$(<\$tmpf))"
    rm -f $tmpf
  else
    if [[ $ZFTP_SYSTEM = UNIX* && $pat = */* ]]; then
      # not the current directory and we know how to handle paths
      if [[ $pat = ?*/* ]]; then
        # careful not to remove too many slashes
        dir=${pat%/*}
      else
        dir=/
      fi
      nondir=${pat##*/}
      zftp ls "$dir" 2>/dev/null >$tmpf
      files=($(<$tmpf))
      files=(${files:t})
      rm -f $tmpf
    else
      # we just have to do an ls and hope that's right
      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)
    fi
    # now we want to see which of the $files match $nondir
    for (( i = 1; i <= $#files; i++)); do
      # empty words are elided in array assignment
      [[ $files[$i] = ${~nondir} ]] || files[$i]=''
    done
    eval "$1=(\$files)"
  fi
}

function zfrtime {
  # Set the modification time of file LOCAL to that of REMOTE.
  # If the optional TIME is passed, it should be in the FTP format
  # CCYYMMDDhhmmSS, i.e. no dot before the seconds, and in GMT.
  # This is what both `zftp remote' and `zftp local' return.
  #
  # Unfortunately, since the time returned from FTP is GMT and
  # your file needs to be set in local time, we need to do some
  # hacking around with time.  At the moment this requires perl 5
  # with the standard library.
  
  setopt localoptions unset
  unsetopt ksharrays
  
  local time gmtime loctime
  
  if [[ -n $3 ]]; then
    time=$3
  else
    time=($(zftp remote $2 2>/dev/null))
    [[ -n $time ]] && time=$time[2]
  fi
  [[ -z $time ]] && return 1
  
  # Now's the real *!@**!?!.  We have the date in GMT and want to turn
  # it into local time for touch to handle.  It's just too nasty
  # to handle in zsh; do it in perl.
  if perl -mTime::Local -e '($file, $t) = @ARGV;
  $yr = substr($t, 0, 4) - 1900;
  $mon = substr($t, 4, 2) - 1;
  $mday = substr($t, 6, 2) + 0;
  $hr = substr($t, 8, 2) + 0;
  $min = substr($t, 10, 2) + 0;
  $sec = substr($t, 12, 2) + 0;
  $time = Time::Local::timegm($sec, $min, $hr, $mday, $mon, $yr);
  utime $time, $time, $file and return 0;' $1 $time 2>/dev/null; then
    print "Setting time for $1 failed.  Need perl 5." 2>1
  fi
  
  # If it wasn't for the GMT/local time thing, it would be this simple.
  #
  # time="${time[1,12]}.${time[13,14]}"
  #
  # touch -t $time $1
  
}

function zfstat {
  # Give a zftp status report using local variables.
  # With option -v, connect to the remote host and ask it what it
  # thinks the status is.  
  
  setopt localoptions unset
  unsetopt ksharrays
  
  local i stat=0 opt optlist verbose
  
  while [[ $1 = -* ]]; do
    if [[ $1 = - || $1 = -- ]]; then
      shift;
      break;
    fi
    optlist=${1#-}
    for (( i = 1; i <= $#optlist; i++)); do
      opt=$optlist[$i]
      case $opt in
        v) verbose=1
  	 ;;
        *) print option $opt not recognised >&2
  	 ;;
      esac
    done
    shift
  done
  
  if [[ -n $ZFTP_HOST ]]; then
    print "Host:\t\t$ZFTP_HOST"
    print "IP:\t\t$ZFTP_IP"
    [[ -n $ZFTP_SYSTEM ]] && print "System type:\t$ZFTP_SYSTEM"
    if [[ -n $ZFTP_USER ]]; then
      print "User:\t\t$ZFTP_USER "
      [[ -n $ZFTP_ACCOUNT ]] && print "Account:\t$AFTP_ACCOUNT"
      print "Directory:\t$ZFTP_PWD"
      print -n "Transfer type:\t"
      if [[ $ZFTP_TYPE = "I" ]]; then
        print Image
      elif [[ $ZFTP_TYPE = "A" ]]; then
        print Ascii
      else
        print Unknown
      fi
      print -n "Transfer mode:\t"
      if [[ $ZFTP_MODE = "S" ]]; then
        print Stream
      elif [[ $ZFTP_MODE = "B" ]]; then
        print Block
      else
        print Unknown
      fi
    else
      print "No user logged in."
    fi
  else
    print "Not connected."
    [[ -n $zflastsession ]] && print "Last session:\t$zflastsession"
    stat=1
  fi
  
  # things which may be set even if not connected:
  [[ -n $ZFTP_REPLY ]] && print "Last reply:\t$ZFTP_REPLY"
  print "Verbosity:\t$ZFTP_VERBOSE"
  print "Timeout:\t$ZFTP_TMOUT"
  print -n "Preferences:\t"
  for (( i = 1; i <= ${#ZFTP_PREFS}; i++ )); do
    case $ZFTP_PREFS[$i] in
      [pP]) print -n "Passive "
  	  ;;
      [sS]) print -n "Sendport "
  	  ;;
      [dD]) print -n "Dumb "
  	  ;;
      *) print -n "$ZFTP_PREFS[$i]???"
    esac
  done
  print
  
  if [[ -n $ZFTP_HOST && $verbose = 1 ]]; then
    zfautocheck -d
    print "Status of remote server:"
    # make sure we print the reply
    local ZFTP_VERBOSE=045
    zftp quote STAT
  fi
  
  return $stat
}

function zftp_chpwd {
  # You may want to alter chpwd to call this when $ZFTP_USER is set.
  
  # Cancel the filename cache for the current directory.
  zftp_fcache=()
  # ...and also empty the stored directory listing cache.
  # As this function is called when we close the connection, this
  # is the only place we need to do these two things.
  [[ -n $zfcurdir && -f $zfcurdir ]] && rm -f $zfcurdir
  zfotherargs=
  
  if [[ -z $ZFTP_USER ]]; then
    # last call, after an FTP logout
  
    # delete the non-current cached directory
    [[ -n $zfotherdir && -f $zfotherdir ]] && rm -f $zfotherdir
  
    # don't keep zflastdir between opens (do keep zflastsession)
    zflastdir=
  
    # return the display to standard
    # uncomment the following line if you have a chpwd which shows directories
    chpwd
  else
    [[ -n $ZFTP_PWD ]] && zflastdir=$ZFTP_PWD
    zflastsession="$ZFTP_HOST:$ZFTP_PWD"
    local args
    if [[ -t 1 && -t 2 ]]; then
      local str=$zflastsession
      [[ ${#str} -lt 70 ]] && str="%m: %~  $str"
      case $TERM in
        sun-cmd) print -n -P "\033]l$str\033\\"
  	       ;;
        xterm) print -n -P "\033]2;$str\a"
  	     ;;
      esac
    fi
  fi
}

function zftp_progress {
  # Basic progress metre, showing the percent of the file transferred.
  # You want growing bars?  You gotta write growing bars.
  
  # Don't show progress unless stderr is a terminal
  [[ ! -t 2 ]] && return 0
  
  if [[ $ZFTP_TRANSFER = *F ]]; then
    print 1>&2
  elif [[ -n $ZFTP_TRANSFER ]]; then
    if [[ -n $ZFTP_SIZE ]]; then
      local frac="$(( ZFTP_COUNT * 100 / ZFTP_SIZE ))%"
      print -n "\r$ZFTP_FILE ($ZFTP_SIZE bytes): $ZFTP_TRANSFER $frac" 1>&2
    else
      print -n "\r$ZFTP_FILE: $ZFTP_TRANSFER $ZFTP_COUNT" 1>&2
    fi
  fi
}

function zftype {
  local type zftmp=${TMPPREFIX}zftype$$
  
  zfautocheck -d
  
  if (( $# == 0 )); then
    zftp type >$zftmp
    type=$(<$zftmp)
    rm -f $zftmp
    if [[ $type = I ]]; then
      print "Current type is image (binary)"
      return 0
    elif [[ $type = A ]]; then
      print "Current type is ASCII"
      return 0
    else
      return 1
    fi
  else
    if [[ $1 == [aA]([sS][cC]([iI][iI]|)|) ]]; then
      type=A
    elif [[ $1 == [iI]([mM]([aA][gG][eE]|)|) ||
      $1 == [bB]([iI][nN]([aA][rR][yY]|)|) ]]; then
      type=I
    else
      print "Type not recognised:  $1" 2>&1
      return 1
    fi
    zftp type $type
  fi
}

function zfuget {
  # Get a list of files from the server with update.
  # In other words, only retrieve files which are newer than local
  # ones.  This depends on the clocks being adjusted correctly
  # (i.e. if one is fifteen minutes out, for the next fifteen minutes
  # updates may not be correctly calculated).  However, difficult
  # cases --- where the files are the same size, but the remote is newer,
  # or have different sizes, but the local is newer -- are prompted for.
  #
  # Files are globbed on the remote host --- assuming, of course, they
  # haven't already been globbed local, so use 'noglob' e.g. as
  # `alias zfuget="noglob zfuget"'.
  #
  # Options:
  #  -G    Glob:     turn off globbing
  #  -v    verbose:  print more about the files listed.
  #  -s    silent:   don't ask, just guess.  The guesses are:
  #                - if the files have different sizes but remote is older ) grab
  #                - if they have the same size but remote is newer        )
  #                  which is safe if the remote files are always the right ones.
  #   -t   time:     update the local file times to the same time as the remote.
  #                  Currently this only works if you have the `perl' command,
  #                  and that perl is version 5 with the standard library.
  #                  See the function zfrtime for more gory details.
  
  setopt localoptions
  unsetopt ksharrays shwordsplit
  
  local loc rem locstats remstats doit tmpfile=${TMPPREFIX}zfuget$$
  local rstat remlist verbose optlist opt bad i silent nglob time
  integer stat do_close
  
  zfuget_print_time() {
    local tim=$1
    print -n "$tim[1,4]/$tim[5,6]/$tim[7,8] $tim[9,10]:$tim[11,12].$tim[13,14]"
    print -n GMT
  }
  
  zfuget_print () {
    print -n "\nremote $rem ("
    zfuget_print_time $remstats[2]
    print -n ", $remstats[1] bytes)\nlocal $loc ("
    zfuget_print_time $locstats[2]
    print ", $locstats[1] bytes)"
  }
  
  while [[ $1 = -* ]]; do
    if [[ $1 = - || $1 = -- ]]; then
      shift;
      break;
    fi
    optlist=${1#-}
    for (( i = 1; i <= $#optlist; i++)); do
      opt=$optlist[$i]
      case $optlist[$i] in
        v) verbose=1
  	 ;;
        s) silent=1
  	 ;;
        G) nglob=1
  	 ;;
        t) time=1
  	 ;;
        *) print option $opt not recognised >&2
  	 ;;
      esac
    done
    shift
  done
  
  [[ -n $bad ]] && return 1
  
  zfautocheck
  
  for remlist in $*; do
    # zfcd directory hack to put the front back to ~
    if [[ $remlist == $HOME || $remlist == $HOME/* ]]; then
      remlist="~${remlist#$HOME}"
    fi
    if [[ $nglob != 1 ]]; then
      zfrglob remlist
    fi
    if (( $#remlist )); then
      for rem in $remlist; do
        loc=${rem:t}
        doit=y
        remstats=()
        if [[ -f $loc ]]; then
  	zftp local $loc >$tmpfile
  	locstats=($(<$tmpfile))
  	zftp remote $rem >$tmpfile
  	rstat=$?
  	remstats=($(<$tmpfile))
  	rm -f $tmpfile
  	if [[ $rstat = 2 ]]; then
  	  print "Server does not implement full command set required." 1>&2
  	  return 1
  	elif [[ $rstat = 1 ]]; then
  	  print "File not found on server: $rem" 1>&2
  	  stat=1
  	  continue
  	fi
  	[[ $verbose = 1 ]] && zfuget_print
  	if (( $locstats[1] != $remstats[1] )); then
  	  # Files have different sizes
  	  if [[ $locstats[2] > $remstats[2] && $silent != 1 ]]; then
  	    [[ $verbose != 1 ]] && zfuget_print
  	    print "Local file $loc more recent than remote," 1>&2
  	    print -n "but sizes are different.  Transfer anyway [y/n]? " 1>&2
  	    read -q doit
  	  fi
  	else
  	  # Files have same size
  	  if [[ $locstats[2] < $remstats[2] ]]; then
  	    if [[ $silent != 1 ]]; then
  	      [[ $verbose != 1 ]] && zfuget_print
  	      print "Local file $loc has same size as remote," 1>&2
  	      print -n "but local file is older. Transfer anyway [y/n]? " 1>&2
  	      read -q doit
  	    fi
  	  else
  	    # presumably same file, so don't get it.
  	    [[ $verbose = 1 ]] && print Not transferring
  	    doit=n
  	  fi
  	fi
        else
  	[[ $verbose = 1 ]] && print New file $loc
        fi
        if [[ $doit = y ]]; then
  	if zftp get $rem >$loc; then
  	  if [[ $time = 1 ]]; then
  	    # if $remstats is set, it's second element is the remote time
  	    zfrtime $loc $rem $remstats[2]
  	  fi
  	else
  	  stat=$?
  	fi
  	
        fi
      done
    fi
  done
  
  (( do_close )) && zfclose
  
  return $stat
}

function zfuput {
  # Put a list of files from the server with update.
  # See zfuget for details.
  #
  # Options:
  #  -v    verbose:  print more about the files listed.
  #  -s    silent:   don't ask, just guess.  The guesses are:
  #                - if the files have different sizes but remote is older ) grab
  #                - if they have the same size but remote is newer        )
  #                  which is safe if the remote files are always the right ones.
  
  setopt localoptions
  unsetopt ksharrays shwordsplit
  
  local loc rem locstats remstats doit tmpfile=${TMPPREFIX}zfuput$$
  local rstat verbose optlist opt bad i silent
  integer stat do_close
  
  zfuput_print_time() {
    local tim=$1
    print -n "$tim[1,4]/$tim[5,6]/$tim[7,8] $tim[9,10]:$tim[11,12].$tim[13,14]"
    print -n GMT
  }
  
  zfuput_print () {
    print -n "\nremote $rem ("
    zfuput_print_time $remstats[2]
    print -n ", $remstats[1] bytes)\nlocal $loc ("
    zfuput_print_time $locstats[2]
    print ", $locstats[1] bytes)"
  }
  
  while [[ $1 = -* ]]; do
    if [[ $1 = - || $1 = -- ]]; then
      shift;
      break;
    fi
    optlist=${1#-}
    for (( i = 1; i <= $#optlist; i++)); do
      opt=$optlist[$i]
      case $optlist[$i] in
        v) verbose=1
  	 ;;
        s) silent=1
  	 ;;
        *) print option $opt not recognised >&2
  	 ;;
      esac
    done
    shift
  done
  
  [[ -n $bad ]] && return 1
  
  zfautocheck
  
  if [[ $ZFTP_VERBOSE = *5* ]]; then
    # should we turn it off locally?
    print "Messages with code 550 are harmless." >&2
  fi
  
  for loc in $*; do
    rem=${loc:t}
    doit=y
    remstats=()
    if [[ ! -f $loc ]]; then
      print "$loc: file not found" >&2
      stat=1
      continue
    fi
    zftp local $loc >$tmpfile
    locstats=($(<$tmpfile))
    zftp remote $rem >$tmpfile
    rstat=$?
    remstats=($(<$tmpfile))
    rm -f $tmpfile
    if [[ $rstat = 2 ]]; then
      print "Server does not implement full command set required." 1>&2
      return 1
    elif [[ $rstat = 1 ]]; then
      [[ $verbose = 1 ]] && print New file $loc
    else
      [[ $verbose = 1 ]] && zfuput_print
      if (( $locstats[1] != $remstats[1] )); then
        # Files have different sizes
        if [[ $locstats[2] < $remstats[2] && $silent != 1 ]]; then
  	[[ $verbose != 1 ]] && zfuput_print
  	print "Remote file $rem more recent than local," 1>&2
  	print -n "but sizes are different.  Transfer anyway [y/n]? " 1>&2
  	read -q doit
        fi
      else
        # Files have same size
        if [[ $locstats[2] > $remstats[2] ]]; then
  	if [[ $silent != 1 ]]; then
  	  [[ $verbose != 1 ]] && zfuput_print
  	  print "Remote file $rem has same size as local," 1>&2
  	  print -n "but remote file is older. Transfer anyway [y/n]? " 1>&2
  	  read -q doit
  	fi
        else
  	# presumably same file, so don't get it.
  	[[ $verbose = 1 ]] && print Not transferring
  	doit=n
        fi
      fi
    fi
    if [[ $doit = y ]]; then
      zftp put $rem <$loc || stat=$?
    fi
  done
  
  (( do_close )) && zfclose
  
  return $stat
}