about summary refs log tree commit diff
path: root/Misc/zftp-functions
diff options
context:
space:
mode:
Diffstat (limited to 'Misc/zftp-functions')
-rw-r--r--Misc/zftp-functions1281
1 files changed, 1281 insertions, 0 deletions
diff --git a/Misc/zftp-functions b/Misc/zftp-functions
new file mode 100644
index 000000000..a07e46d72
--- /dev/null
+++ b/Misc/zftp-functions
@@ -0,0 +1,1281 @@
+# zftp is a loadable module implementing an FTP client as a builtin
+# command so that you can use the shell command language and line
+# editing to make life easier.  If your system has dynamically
+# load libraries and zsh was compiled to use them, it is probably
+# somewhere where it can be loaded at run time.  Otherwise, it depends
+# whether the shell was compiled with zftp already built into it.
+#
+# Here is a suite of functions, plus assorted other code, to make
+# zftp work smoothly.
+#
+# Completion is implemented in a fairly natural way, except that
+# very little support has been provided for non-UNIX remote hosts.
+# On such machines, the safest thing to do is only try to complete
+# files in the current directory; this should be OK.
+#
+# Remote globbing for commands which retrieve files is also
+# implemented.  This can be done in two different ways.  The default
+# is for zsh to do the globbing locally.  The advantage is that full
+# zsh pattern matching (respecting the setting of extendedglob) is
+# possible, and no assumption (apart from the restrictions on
+# directory handling noted above) is made about the behaviour of the
+# server.  The disadvantage is that the entire filename list for the
+# current directory must be retrieved, and then zsh must laboriously
+# do pattern matching against every file, so it is potentially slow
+# for large directories.  Only the non-directory part of file names is
+# globbed.
+#
+# The alternative will be used if $zfrglob has non-zero length.
+# Zsh then sends the pattern to the server for globbing.  Best of
+# luck.
+#
+# To support remote globbing, some functions have been aliased
+# with 'noglob' in front.  Currently, this has a dire effect on
+# completion unless the completeinaliases option is set, so
+# it is set below.  This can conceivably cause you problems
+# if you expect completion for aliases automatically to give you
+# completion for the base command.  I suspect that most people
+# don't even know that happens.
+#
+# The following functions are provided.
+#
+# General status changing and displaying functions:
+# zfparams
+#   Simple front end to `zftp params', except it will automatically
+#   query host, user and password.  These are then stored to be
+#   used with a `zfopen' with no arguments.
+# zfopen [ host [ user ... ] ]
+#   Open a connection and login.  Unless the option -1 (once)
+#   is given, will store the parameters for the open (including
+#   a password which is prompted for and not echoed) so that
+#   if you call zfopen subsequently without arguments it will
+#   reopen the same connection.
+# zfanon anonftphost
+#   Open a connection for anonymous FTP.  Tries to guess an
+#   email address to use as the password, unless $EMAIL_ADDR is
+#   already set.  The first time, will tell you what it has guessed.
+#   It's rude to set EMAIL_ADDR=mozilla.
+# zfcd [ dir | old new ]
+#   Change directory on the server.  This tries to mimic the behaviour
+#   of the shell's cd.  In particular,
+#    zfcd           change to '~' on server, if it interprets it
+#    zfcd -         change to previous directory of current connection
+#    zfcd OLD NEW   change directory from fooOLDbar to fooNEWbar
+#   One piece of magic is builtin:  an initial part of the directory
+#   matching $HOME is translated back to `~'.  Most UNIX servers
+#   recognise the usual shell convention.  So things like `zfcd $PWD'
+#   is useful provide you are under your home directory and the
+#   structure on the remote machine mirrors that on the local.
+# zfhere
+#   Synonym for `zfcd $PWD', see above.
+# zfdir [args]
+#   Show a long diretory list of the remote connection.  Any
+#   arguments are passed on to the server, apart from options.
+#   Currently this always uses a pager to show the directory
+#   list.  Caching is implemented:  zfdir on its own always shows
+#   the current diretory, which is cached; zfdir with some other
+#   directory arguments shows that, which is cached separately
+#   and can be reviewed with `zfdir -r'.  Other options:
+#    -f  force reget, overriding the cache, in case something's changed
+#    -d  delete the cache, but don't show anything.
+#   To pass options to the server, use e.g. `zfdir -- -C'.
+#   This also has the zfcd ~ hack.
+# zfls [args]
+#   Short list of the long directory, depending on what [args]
+#   do to the server.  No options, no caching, no pager.
+# zftype [ a[scii] | i[mage] | b[inary] ]
+#   Set or display the transfer type; currently only ASCII
+#   and image (same as binary) types are supported.
+# zfclose
+#   Close the connection.
+# zfstat
+#   Print the zftp status from local variables; doesn't do any network
+#   operations unless -v is supplied, in which case the server is
+#   asked for its views on the status, too.
+#
+# Functions for retrieving data:
+#   All accept the following options:
+#    -G   Don't do remote globbing (see above); the default is to do it.
+#    -t   Try to set local files to the same time as the remote ones.
+#         Unfortunately we only know the remote time in GMT, so it's
+#         a little tricky and you need perl 5 (installed as `perl')
+#         for this to work.  Suggestions welcome.
+# zfget file1 file2 ...
+#   Retrieve each file from the server.  The remote file is the
+#   full name given, the local file is the non-directory part of that
+#   (assuming UNIX file paths).
+# zfuget file1 file2 ..
+#   Get with update.  Check remote and local sizes and times and
+#   retrieve files which are newer on the server.  Will query
+#   hard cases, which are where the remote file is newer but a
+#   different size, or is older but the same size.  With option -s
+#   (silent) assumes it's best to retrieve the files in both those
+#   cases.  With -v (may be combined with -s), print the information
+#   about the files being considered.
+# zfcget file1 ...
+#   Assuming file1 was incompletely retrieved, try to get the rest of
+#   it.  This relies on a normal UNIX server behaviour which is not
+#   as specified in the FTP standard and hence is not universal.
+# zfgcp file1 file2
+# zfgcp file1 file2 ... dir
+#   Get with the behaviour of cp, i.e. copy remote file1 to local
+#   file2, or get remote fileN into local diretory dir.
+#
+# Function for sending data:
+# zfput file1 file2 ...
+#   Put the local files onto the server under the same name.  The
+#   local files are exactly as given; the remote files are the
+#   non-diretory parts of that. 
+# zfuput file1 file2 ..
+#   Put the local files onto the server, with update.  Works
+#   similarly to zfuget.
+#
+# Utility functions:
+# zftp_chpwd
+#   Show the new directory when it changes; try to put it into
+#   an xterm on shelltool header.  Works best alongside chpwd.
+# zftp_progress
+#   Show the percentage of a file retrieved as it is coming; if the
+#   size is not available show the size transferred so far.  The
+#   percentage may be wrong if sending data from a local pipe.
+#   If you transfer files in the background, you should undefine
+#   this before the transfer.  It is smart enough not to print
+#   anything when stderr is not a terminal.
+# zfcd_match
+#   Function for remote directory completion.
+# zfget_match
+#   Function for remote filename completion.
+# zfrglob varname
+#   This is used for the remote globbing.  The pattern resides
+#   in $varname (note extra level of indirection), and on return
+#   $varname will contain the list of matching files.
+# zfrtime locfile remfile [ time ]
+#   This sad thing does the setting of local file times to those
+#   of the remote, see horror story above.
+
+zmodload -ia zftp
+
+alias zfcd='noglob zfcd'
+alias zfget='noglob zfget'
+alias zfls='noglob zfls'
+alias zfdir='noglob zfdir'
+alias zfuget='noglob zfuget'
+# only way of getting that noglob out of the way at the moment
+setopt completealiases
+
+#
+# zftp completions
+#
+compctl -f -x 'p[1]' \
+  -k '(open params user login type ascii binary mode put putat
+  get getat append appendat ls dir local remote mkdir rmdir delete
+  close quit)'  - \
+  'w[1,cd][1,ls][1,dir][1,rmdir]' -K zfcd_match -S/ -q - \
+  'W[1,get*]' -K zfget_match - 'w[1,delete][1,remote]' -K zfget_match - \
+  'w[1,open][1,params]' -k hosts -- zftp
+compctl -K zfcd_match -S/ -q zfcd zfdir zfls
+compctl -K zfget_match zfget zfgcp zfuget zfcget
+compctl -k hosts zfopen zfparams
+
+function zfanon {
+  local opt optlist once
+  
+  while [[ $1 = -* ]]; do
+    if [[ $1 = - || $1 = -- ]]; then
+      shift;
+      break;
+    fi
+    optlist=${1#-}
+    for (( i = 1; i <= $#optlist; i++)); do
+      opt=$optlist[$i]
+      case $optlist[$i] in
+        1) once=1
+  	 ;;
+        *) print option $opt not recognised >&2
+  	 ;;
+      esac
+    done
+    shift
+  done
+  
+  if [[ -z $EMAIL_ADDR ]]; then
+    # Exercise in futility.  There's a poem by Wallace Stevens
+    # called something like `N ways of looking at a blackbird',
+    # where N is somewhere around 0x14 to 0x18.  Now zftp is
+    # ashamed to prsent `N ways of looking at a hostname'.
+    local domain host
+    # First, maybe we've already got it.  Zen-like.
+    if [[ $HOST = *.* ]]; then
+      # assume this is the full host name
+      host=$HOST
+    elif [[ -f /etc/resolv.conf ]]; then
+      # Next, maybe we've got resolv.conf.
+      domain=$(awk '/domain/ { print $2 }' /etc/resolv.conf)
+      [[ -n $domain ]] && host=$HOST.$domain
+    fi
+    # Next, maybe we've got nlsookup.  May not work on LINUX.
+    [[ -z $host ]] && host=$(nslookup $HOST | awk '/Name:/ { print $2 }')
+    if [[ -z $host ]]; then
+      # we're running out of ideas, but this should work.
+      # after all, i wrote it...
+      # don't want user to know about this, too embarrassed.
+      local oldvb=$ZFTP_VERBOSE oldtm=$ZFTP_TMOUT
+      ZFTP_VERBOSE=
+      ZFTP_TMOUT=5
+      if zftp open $host >& /dev/null; then
+        host=$ZFTP_HOST
+        zftp close $host
+      fi
+      ZFTP_VERBOSE=$oldvb
+      ZFTP_TMOUT=$oldtm
+    fi
+    if [[ -z $host ]]; then
+      print "Can't get your hostname.  Define \$EMAIL_ADDR by hand."
+      return 1;
+    fi
+    EMAIL_ADDR="$USER@$host"
+    print "Using $EMAIL_ADDR as anonymous FTP password."
+  fi
+  
+  if [[ $once = 1 ]]; then
+    zftp open $1 anonymous $EMAIL_ADDR
+  else
+    zftp params $1 anonymous $EMAIL_ADDR
+    zftp open
+  fi
+}
+
+function zfcd {
+  # zfcd:  change directory on the remote server.
+  #
+  #  Currently has the following features:
+  # --- an initial string matching $HOME in the directory is turned back into ~
+  #     to be re-interpreted by the remote server.
+  # --- zfcd with no arguments changes directory to '~'
+  # --- `zfcd old new' and `zfcd -' work analagously to cd
+  # --- if the connection is not currently open, it will try to
+  #     re-open it with the stored parameters as set by zfopen.
+  #     If the connection timed out, however, it won't know until
+  #     too late.  In that case, just try the same zfcd command again
+  #     (but now `zfcd -' and `zfcd old new' won't work).
+  
+  # hack: if directory begins with $HOME, turn it back into ~
+  # there are two reasons for this:
+  #   first, a ~ on the command line gets expanded even with noglob.
+  #     (I suppose this is correct, but I wouldn't like to swear to it.)
+  #   second, we can no do 'zfcd $PWD' and the like, and that will
+  #     work just as long as the directory structures under the home match.
+  
+  # Autoopen:  if not already open, hope there are parameters set up to
+  # do so.  If not, we get the right error message, so no harm done.
+  [[ -z $ZFTP_HOST ]] && { zfopen  || return 1; }
+  
+  if [[ $1 = $HOME || $1 = $HOME/* ]]; then
+    1="~${1#$HOME}"
+  fi
+  
+  if (( $# == 0 )); then
+    # Emulate `cd' behaviour
+    set -- '~'
+  elif [[ $# -eq 1 && $1 = - ]]; then
+    # Emulate `cd -' behaviour.
+    set -- $zflastdir
+  elif [[ $# -eq 2 ]]; then
+    # Emulate `cd old new' behaviour.
+    # We have to find a character not in $1 or $2; ! is a good bet.
+    eval set -- "\${ZFTP_PWD:s!$1!$2!}"
+  fi
+  
+  # We have to remember the current directory before changing it
+  # if we want to keep it.
+  local lastdir=$ZFTP_PWD
+  
+  zftp cd "$@"  &&  zflastdir=$lastdir
+}
+
+function zfcd_match {
+  # see zfcd for details of this hack
+  if [[ $1 = $HOME || $1 = $HOME/* ]]; then
+    1="~${1#$HOME}"
+  fi
+  
+  # error messages only
+  local ZFTP_VERBOSE=45
+  # should we redirect 2>/dev/null or let the user see it?
+  
+  if [[ $ZFTP_SYSTEM = UNIX* ]]; then
+    # hoo, aren't we lucky: this makes things so much easier
+    setopt localoptions rcexpandparam
+    local dir
+    if [[ $1 = ?*/* ]]; then
+      dir=${1%/*}
+    elif [[ $1 = /* ]]; then
+      dir=/
+    fi
+    # If we're using -F, we get away with using a directory
+    # to list, but not a glob.  Don't ask me why.
+    # I hate having to rely on awk here.
+    reply=($(zftp ls -F $dir | 
+    awk '/\/$/ { print substr($1, 0, length($1)-1) }'))
+    if [[ $dir = / ]]; then
+      reply=(${dir}$reply)
+    elif [[ -n $dir ]]; then
+      reply=($dir/$reply)
+    fi
+  else
+    # I simply don't know what to do here.
+    # Just use the list of files for the current directory.
+    zfget_match $*
+  fi
+  
+}
+
+function zfcget {
+  # Continuation get of files from remote server.
+  # For each file, if it's shorter here, try to get the remainder from
+  # over there.  This requires the server to support the REST command
+  # in the way many do but RFC959 doesn't specify.
+  # Options:
+  #   -G   don't to remote globbing, else do
+  #   -t   update the local file times to the same time as the remote.
+  #        Currently this only works if you have the `perl' command,
+  #        and that perl is version 5 with the standard library.
+  #        See the function zfrtime for more gory details.
+  
+  setopt localoptions
+  unsetopt ksharrays shwordsplit
+  
+  local loc rem stat=0 optlist opt nglob remlist locst remst
+  local tmpfile=${TMPPREFIX}zfcget$$ rstat tsize time
+  
+  while [[ $1 = -* ]]; do
+    if [[ $1 = - || $1 = -- ]]; then
+      shift;
+      break;
+    fi
+    optlist=${1#-}
+    for (( i = 1; i <= $#optlist; i++)); do
+      opt=$optlist[$i]
+      case $optlist[$i] in
+        G) nglob=1
+  	 ;;
+        t) time=1
+  	 ;;
+        *) print option $opt not recognised >&2
+  	 ;;
+      esac
+    done
+    shift
+  done
+  
+  for remlist in $*; do
+    # zfcd directory hack to put the front back to ~
+    if [[ $remlist = $HOME || $remlist = $HOME/* ]]; then
+      remlist="~${remlist#$HOME}"
+    fi
+    if [[ $nglob != 1 ]]; then
+      zfrglob remlist
+    fi
+    if (( $#remlist )); then
+      for rem in $remlist; do
+        loc=${rem:t}
+        if [[ ! -f $loc ]]; then
+  	# File does not yet exist
+  	zftp get $rem >$loc || stat=$?
+        else
+  	# Compare the sizes.
+  	locst=($(zftp local $loc))
+  	zftp remote $rem >$tmpfile
+  	rstat=$?
+  	remst=($(<$tmpfile))
+  	rm -f $tmpfile
+  	if [[ $rstat = 2 ]]; then
+  	  print "Server does not support SIZE command.\n" \
+  	  "Assuming you know what you're doing..." 2>&1
+  	  zftp getat $rem $locst[1] >>$loc || stat=$?
+  	  continue
+  	elif [[ $rstat = 1 ]]; then
+  	  print "Remote file not found: $rem" 2>&1
+  	  continue
+  	fi
+  	if [[ $locst[1] -gt $remst[1] ]]; then
+  	  print "Local file is larger!" 2>&1
+  	  continue;
+  	elif [[ $locst[1] == $remst[1] ]]; then
+  	  print "Files are already the same size." 2>&1
+  	  continue
+  	else
+  	  if zftp getat $rem $locst[1] >>$loc; then
+  	    [[ $time = 1 ]] && zfrtime $loc $rem $remst[2]
+  	  else
+  	    stat=1
+  	  fi
+  	fi
+        fi
+      done
+    fi
+  done
+  
+  return $stat
+}
+
+function zfclose {
+  zftp close
+}
+
+function zfdir {
+  # Long directory of remote server.
+  # The remote directory is cached.  In fact, two caches are kept:
+  # one of the standard listing of the current directory, i.e. zfdir
+  # with no arguments, and another for everything else.
+  # To access the appropriate cache, just use zfdir with the same
+  # arguments as previously.  zfdir -r will also re-use the `everything
+  # else' cache; you can always reuse the current directory cache just
+  # with zfdir on its own.
+  #
+  # The current directory cache is emptied when the directory changes;
+  # the other is kept until a new zfdir with a non-empty argument list.
+  # Both are removed when the connection is closed.
+  #
+  # zfdir -f will force the existing cache to be ignored, e.g. if you know
+  #          or suspect the directory has changed.
+  # zfdir -d will remove both caches without listing anything.
+  # If you need to pass -r, -f or -d to the dir itself, use zfdir -- -d etc.
+  
+  setopt localoptions unset
+  unsetopt shwordsplit ksharrays
+  
+  local file opt optlist redir i newargs force
+  
+  while [[ $1 = -* ]]; do
+    if [[ $1 = - || $1 = -- ]]; then
+      shift;
+      break;
+    fi
+    optlist=${1#-}
+    for (( i = 1; i <= $#optlist; i++)); do
+      opt=$optlist[$i]
+      case $optlist[$i] in
+        r) redir=1
+  	 ;;
+        f) force=1
+  	 ;;
+        d) [[ -n $zfcurdir && -f $zfcurdir ]] && rm -f $zfcurdir
+  	 [[ -n $zfotherdir && -f $zfotherdir ]] && rm -f $zfotherdir
+  	 zftp_fcache=()
+  	 return 0
+  	 ;;
+        *) print option $opt not recognised >&2
+  	 ;;
+      esac
+    done
+    shift
+  done
+  
+  # directory hack, see zfcd
+  for (( i = 1; i <= $#argv; i++ )); do
+    if [[ $argv[$i] = $HOME || $argv[$i] = $HOME/* ]]; then
+      argv[$i]="~${argv[$i]#$HOME}"
+    fi
+  done
+  
+  if [[ $# -eq 0 ]]; then
+    # Cache it in the current directory file.  This means that repeated
+    # calls to zfdir with no arguments always use a cached file.
+    [[ -z $zfcurdir ]] && zfcurdir=${TMPPREFIX}zfcurdir$$
+    file=$zfcurdir
+  else
+    # Last directly looked at was not the current one, or at least
+    # had non-standard arguments.
+    [[ -z $zfotherdir ]] && zfotherdir=${TMPPREFIX}zfotherdir$$
+    file=$zfotherdir
+    newargs="$*"
+    if [[ -f $file && $redir != 1 && $force -ne 1 ]]; then
+      # Don't use the cached file if the arguments changed.
+      [[ $newargs = $zfotherargs ]] || rm -f $file
+    fi
+    zfotherargs=$newargs
+  fi
+  
+  if [[ $force -eq 1 ]]; then
+    rm -f $file
+    # if it looks like current directory has changed, better invalidate
+    # the filename cache, too.
+    (( $# == 0 )) && zftp_fcache=()
+  fi
+  
+  if [[ -n $file && -f $file ]]; then
+    eval ${PAGER:-more} \$file
+  else
+    zftp dir $* | tee $file | eval ${PAGER-:more}
+  fi
+}
+
+function zfgcp {
+  # ZFTP get as copy:  i.e. first arguments are remote, last is local.
+  # Supposed to work exactly like a normal copy otherwise, i.e.
+  #  zfcp rfile lfile
+  # or
+  #  zfcp rfile1 rfile2 rfile3 ... ldir
+  # Options:
+  #   -G   don't to remote globbing, else do
+  #   -t   update the local file times to the same time as the remote.
+  #        Currently this only works if you have the `perl' command,
+  #        and that perl is version 5 with the standard library.
+  #        See the function zfrtime for more gory details.
+  
+  setopt localoptions
+  unsetopt shwordsplit
+  
+  local opt optlist nglob remlist rem loc stat=0 time
+  
+  while [[ $1 == -* ]]; do
+    if [[ $1 == - || $1 == -- ]]; then
+      shift;
+      break;
+    fi
+    optlist=${1#-}
+    for (( i = 1; i <= $#optlist; i++)); do
+      opt=$optlist[$i]
+      case $opt in
+        G) nglob=1
+  	 ;;
+        t) time=1
+  	 ;;
+        *) print option $opt not recognised >&2
+  	 ;;
+      esac
+    done
+    shift
+  done
+  
+  # hmm, we should really check this after expanding the glob,
+  # but we shouldn't expand the last argument remotely anyway.
+  if [[ $# -gt 2 && ! -d $argv[-1] ]]; then
+    print "zfgcp:  last argument must be a directory." 2>&1
+    return 1
+  elif [[ $# == 1 ]]; then
+    print "zfgcp:  not enough arguments." 2>&1
+    return 1
+  fi
+  
+  if [[ -d $argv[-1] ]]; then
+    local dir=$argv[-1]
+    argv[-1]=
+    for remlist in $*; do
+      # zfcd directory hack to put the front back to ~
+      if [[ $remlist = $HOME || $remlist = $HOME/* ]]; then
+        remlist="~${remlist#$HOME}"
+      fi
+      if [[ $nglob != 1 ]]; then
+        zfrglob remlist
+      fi
+      if (( $#remlist )); then
+        for rem in $remlist; do
+  	loc=$dir/${rem:t}
+  	if zftp get $rem >$loc; then
+  	  [[ $time = 1 ]] && zfrtime $rem $loc
+  	else
+  	  stat=1
+  	fi
+        done
+      fi
+    done
+  else
+    zftp get $1 >$2 || stat=$?
+  fi
+  return $stat
+}
+
+function zfget {
+  # Get files from remote server.  Options:
+  #   -G   don't to remote globbing, else do
+  #   -t   update the local file times to the same time as the remote.
+  #        Currently this only works if you have the `perl' command,
+  #        and that perl is version 5 with the standard library.
+  #        See the function zfrtime for more gory details.
+  
+  local loc rem stat=0 optlist opt nglob remlist time
+  
+  while [[ $1 == -* ]]; do
+    if [[ $1 == - || $1 == -- ]]; then
+      shift;
+      break;
+    fi
+    optlist=${1#-}
+    for (( i = 1; i <= $#optlist; i++)); do
+      opt=$optlist[$i]
+      case $opt in
+        G) nglob=1
+  	 ;;
+        t) time=1
+  	 ;;
+        *) print option $opt not recognised >&2
+  	 ;;
+      esac
+    done
+    shift
+  done
+  
+  for remlist in $*; do
+    # zfcd directory hack to put the front back to ~
+    if [[ $remlist == $HOME || $remlist == $HOME/* ]]; then
+      remlist="~${remlist#$HOME}"
+    fi
+    if [[ $nglob != 1 ]]; then
+      zfrglob remlist
+    fi
+    if (( $#remlist )); then
+      for rem in $remlist; do
+        loc=${rem:t}
+        if zftp get $rem >$loc; then
+  	[[ $time = 1 ]] && zfrtime $rem $loc
+        else
+  	stat=1
+        fi
+      done
+    fi
+  done
+  
+  return $stat
+}
+
+function zfget_match {
+  # the zfcd hack:  this may not be necessary here
+  if [[ $1 == $HOME || $1 == $HOME/* ]]; then
+    1="~${1#$HOME}"
+  fi
+  
+  
+  if [[ $ZFTP_SYSTEM == UNIX* && $1 == */* ]]; then
+    # On the first argument to ls, we usually get away with a glob.
+    reply=($(zftp ls "$1*$2"))
+  else
+    if (( $#zftp_fcache == 0 )); then
+      # Always cache the current directory and use it
+      # even if the system is UNIX.
+      zftp_fcache=($(zftp ls))
+    fi
+    reply=($zftp_fcache);
+  fi
+}
+
+function zfhere {
+  # Change to the directory corresponding to $PWD on the server.
+  # See zfcd for how this works.
+  zfcd $PWD
+}
+
+function zfls {
+  # directory hack, see zfcd
+  if [[ $1 = $HOME || $1 = $HOME/* ]]; then
+    1="~${1#$HOME}"
+  fi
+  zftp ls $*
+}
+
+function zfopen {
+  # Use zftp params to set parameters for open, rather than sending
+  # them straight to open.  That way they are stored for a future open
+  # command.
+  #
+  # With option -1 (just this 1ce), don't do that.
+  
+  local optlist opt once
+  
+  while [[ $1 = -* ]]; do
+    if [[ $1 = - || $1 = -- ]]; then
+      shift;
+      break;
+    fi
+    optlist=${1#-}
+    for (( i = 1; i <= $#optlist; i++)); do
+      opt=$optlist[$i]
+      case $optlist[$i] in
+        1) once=1
+  	 ;;
+        *) print option $opt not recognised >&2
+  	 ;;
+      esac
+    done
+    shift
+  done
+  
+  # This is where we should try and do same name-lookupage in
+  # both .netrc and .ncftp/bookmarks .  We could even try saving
+  # the info in their for new hosts, like ncftp does.
+  
+  if [[ $once = 1 ]]; then
+    zftp open $*
+  else
+    # set parameters, but only if there was at least a host
+    (( $# > 0 )) && zfparams $*
+    # now call with no parameters
+    zftp open
+  fi
+}
+
+function zfparams {
+    # Set to prompt for any user or password if not given.
+    # Don't worry about accounts here.
+    if (( $# > 0 )); then
+      (( $# < 2 )) && 2='?'
+      (( $# < 3 )) && 3='?'
+    fi
+    zftp params $*
+}
+
+function zfput {
+  # Simple put:  dump every file under the same name, but stripping
+  # off any directory parts.
+  local loc rem stat=0
+  for loc in $*; do
+    rem=${loc:t}
+    zftp put $rem <$loc
+    [[ $? == 0 ]] || stat=$?
+  done
+  return $stat
+}
+
+function zfrglob {
+  # Do the remote globbing for zfput, etc.
+  # We have two choices:
+  #  (1) Get the entire file list and match it one by one
+  #      locally against the pattern.
+  #      Causes problems if we are globbing directories (rare, presumably).
+  #      But: we can cache the current directory, which
+  #      we need for completion anyway.  Works on any OS if you
+  #      stick with a single directory.  This is the default.
+  #  (2) Use remote globbing, i.e. pass it to ls at the site.
+  #      Faster, but only works with UNIX, and only basic globbing.
+  #      We do this if $zfrglob is non-null.
+  
+  # There is only one argument, the variable containing the
+  # pattern to be globbed.  We set this back to an array containing
+  # all the matches.
+  setopt localoptions unset
+  unsetopt ksharrays
+  
+  local pat dir nondir files i
+  
+  eval pat=\$$1
+  
+  # Check if we really need to do anything.  Look for standard
+  # globbing characters, and if extendedglob is set and we are
+  # using zsh for the actual pattern matching also look for
+  # extendedglob characters.
+  if [[ $remlist != *[][*?]* &&
+    ( -n $zfrglob || ! -o extendedglob || $remlist != *[(|)~#^]* ) ]]; then
+    return 0
+  fi
+  
+  if [[ $zfrglob != '' ]]; then
+    eval "$1=(\$(zftp ls \"$pat\" 2>/dev/null))"
+  else
+    if [[ $ZFTP_SYSTEM = UNIX* && $pat = */* ]]; then
+      # not the current directory and we know how to handle paths
+      if [[ $pat = ?*/* ]]; then
+        # careful not to remove too many slashes
+        dir=${pat%/*}
+      else
+        dir=/
+      fi
+      nondir=${pat##*/}
+      files=($(zftp ls "$dir" 2>/dev/null))
+    else
+      # we just have to do an ls and hope that's right
+      nondir=$pat
+      if (( $#zftp_fcache == 0 )); then
+        zftp_fcache=($(zftp ls))
+      fi
+      files=($zftp_fcache)
+    fi
+    # now we want to see which of the $files match $nondir
+    for (( i = 1; i <= $#files; i++)); do
+      # empty words are elided in array assignment
+      [[ $files[$i] = ${~nondir} ]] || files[$i]=''
+    done
+    eval "$1=(\$files)"
+  fi
+}
+
+function zfrtime {
+  # Set the modification time of file LOCAL to that of REMOTE.
+  # If the optional TIME is passed, it should be in the FTP format
+  # CCYYMMDDhhmmSS, i.e. no dot before the seconds, and in GMT.
+  # This is what both `zftp remote' and `zftp local' return.
+  #
+  # Unfortunately, since the time returned from FTP is GMT and
+  # your file needs to be set in local time, we need to do some
+  # hacking around with time.  At the moment this requires perl 5
+  # with the standard library.
+  
+  setopt localoptions unset
+  unsetopt ksharrays
+  
+  local time gmtime loctime
+  
+  if [[ -n $3 ]]; then
+    time=$3
+  else
+    time=($(zftp remote $2 2>/dev/null))
+    [[ -n $time ]] && time=$time[2]
+  fi
+  [[ -z $time ]] && return 1
+  
+  # Now's the real *!@**!?!.  We have the date in GMT and want to turn
+  # it into local time for touch to handle.  It's just too nasty
+  # to handle in zsh; do it in perl.
+  if perl -mTime::Local -e '($file, $t) = @ARGV;
+  $yr = substr($t, 0, 4) - 1900;
+  $mon = substr($t, 4, 2) - 1;
+  $mday = substr($t, 6, 2) + 0;
+  $hr = substr($t, 8, 2) + 0;
+  $min = substr($t, 10, 2) + 0;
+  $sec = substr($t, 12, 2) + 0;
+  $time = Time::Local::timegm($sec, $min, $hr, $mday, $mon, $yr);
+  utime $time, $time, $file and return 0;' $1 $time 2>/dev/null; then
+    print "Setting time for $1 failed.  Need perl 5." 2>1
+  fi
+  
+  # If it wasn't for the GMT/local time thing, it would be this simple.
+  #
+  # time="${time[1,12]}.${time[13,14]}"
+  #
+  # touch -t $time $1
+  
+}
+
+function zfstat {
+  # Give a zftp status report using local variables.
+  # With option -v, connect the remote host and ask it what it
+  # thinks the status is.  
+  
+  setopt localoptions unset
+  unsetopt ksharrays
+  
+  local i stat=0 opt optlist verbose
+  
+  while [[ $1 = -* ]]; do
+    if [[ $1 = - || $1 = -- ]]; then
+      shift;
+      break;
+    fi
+    optlist=${1#-}
+    for (( i = 1; i <= $#optlist; i++)); do
+      opt=$optlist[$i]
+      case $opt in
+        v) verbose=1
+  	 ;;
+        *) print option $opt not recognised >&2
+  	 ;;
+      esac
+    done
+    shift
+  done
+  
+  # hack:  in case the status from a subshell process hasn't been
+  # fixed yet
+  zftp type >&/dev/null
+  
+  if [[ -n $ZFTP_HOST ]]; then
+    print "Host:\t\t$ZFTP_HOST"
+    print "IP:\t\t$ZFTP_IP"
+    [[ -n $ZFTP_SYSTEM ]] && print "System type:\t$ZFTP_SYSTEM"
+    if [[ -n $ZFTP_USER ]]; then
+      print "User:\t\t$ZFTP_USER "
+      [[ -n $ZFTP_ACCOUNT ]] && print "Account:\t$AFTP_ACCOUNT"
+      print "Directory:\t$ZFTP_PWD"
+      print -n "Transfer type:\t"
+      if [[ $ZFTP_TYPE = "I" ]]; then
+        print Image
+      elif [[ $ZFTP_TYPE = "A" ]]; then
+        print Ascii
+      else
+        print Unknown
+      fi
+      print -n "Transfer mode:\t"
+      if [[ $ZFTP_MODE = "S" ]]; then
+        print Stream
+      elif [[ $ZFTP_MODE = "B" ]]; then
+        print Block
+      else
+        print Unknown
+      fi
+    else
+      print "No user logged in."
+    fi
+  else
+    print "Not connected."
+    stat=1
+  fi
+  
+  # things which may be set even if not connected:
+  [[ -n $ZFTP_REPLY ]] && print "Last reply:\t$ZFTP_REPLY"
+  print "Verbosity:\t$ZFTP_VERBOSE"
+  print -n "Preferences:\t"
+  for (( i = 1; i <= ${#ZFTP_PREFS}; i++ )); do
+    case $ZFTP_PREFS[$i] in
+      [pP]) print -n "Passive "
+  	  ;;
+      [sS]) print -n "Sendport "
+  	  ;;
+      [dD]) print -n "Dumb "
+  	  ;;
+      *) print -n "$ZFTP_PREFS[$i]???"
+    esac
+  done
+  print
+  
+  if [[ -n $ZFTP_HOST && $verbose = 1 ]]; then
+    print "Status of remote server:"
+    # make sure we print the reply
+    local ZFTP_VERBOSE=045
+    zftp quote STAT
+  fi
+  
+  return $stat
+}
+
+function zftp_chpwd {
+  # You may want to alter chpwd to call this when $ZFTP_USER is set.
+  # If so, call it with a non-zero first argument so it doesn't
+  # print the new FTP directory.
+  
+  # Cancel the filename cache for the current directory.
+  zftp_fcache=()
+  # ...and also empty the stored directory listing cache.
+  # As this function is called when we close the connection, this
+  # is the only place we need to do these two things.
+  [[ -n $zfcurdir && -f $zfcurdir ]] && rm -f $zfcurdir
+  
+  if [[ -z $ZFTP_USER ]]; then
+    # last call, after an FTP logout
+  
+    # delete the non-current cached directory
+    [[ -n $zfotherdir && -f $zfotherdir ]] && rm -f $zfotherdir
+    zfotherargs=
+  
+    # don't keep zflastdir between opens
+    zflastdir=
+  
+    # return the display to standard
+    # uncomment the following line if you have a chpwd which shows directories
+    # chpwd
+  else
+    [[ -z $zflastdir ]] && zflastdir=$ZFTP_PWD
+    local args
+    if [[ -t 1 && -t 2 ]]; then
+      local str="$ZFTP_HOST:$ZFTP_PWD"
+      [[ -z $1 ]] && print $str
+      [[ ${#str} -lt 70 ]] && str="%m: %~  $str"
+      case $TERM in
+        sun-cmd) print -n -P "\033]l$str\033\\"
+  	       ;;
+        xterm) print -n -P "\033]2;$str\a"
+  	     ;;
+      esac
+    fi
+  fi
+}
+
+function zftp_progress {
+  # Basic progress metre, showing the percent of the file transferred.
+  # You want growing bars?  You gotta write growing bars.
+  
+  # Don't show progress unless stderr is a terminal
+  [[ ! -t 2 ]] && return 0
+  
+  if [[ $ZFTP_TRANSFER = *F ]]; then
+    print 1>&2
+  elif [[ -n $ZFTP_TRANSFER ]]; then
+    if [[ -n $ZFTP_SIZE ]]; then
+      local frac="$(( ZFTP_COUNT * 100 / ZFTP_SIZE ))%"
+      print -n "\r$ZFTP_FILE ($ZFTP_SIZE bytes): $ZFTP_TRANSFER $frac" 1>&2
+    else
+      print -n "\r$ZFTP_FILE: $ZFTP_TRANSFER $ZFTP_COUNT" 1>&2
+    fi
+  fi
+}
+
+function zftype {
+  local type
+  
+  if (( $# == 0 )); then
+    type=$(zftp type)
+    if [[ $type = I ]]; then
+      print "Current type is image (binary)"
+      return 0
+    elif [[ $type = A ]]; then
+      print "Current type is ASCII"
+      return 0
+    else
+      return 1
+    fi
+  else
+    if [[ $1 == [aA]([sS][cC]([iI][iI]|)|) ]]; then
+      type=A
+    elif [[ $1 == [iI]([mM]([aA][gG][eE]|)|) ||
+      $1 == [bB]([iI][nN]([aA][rR][yY]|)|) ]]; then
+      type=I
+    else
+      print "Type not recognised:  $1" 2>&1
+      return 1
+    fi
+    zftp type $type
+  fi
+}
+
+function zfuget {
+  # Get a list of files from the server with update.
+  # In other words, only retrieve files which are newer than local
+  # ones.  This depends on the clocks being adjusted correctly
+  # (i.e. if one is fifteen minutes out, for the next fifteen minutes
+  # updates may not be correctly calculated).  However, difficult
+  # cases --- where the files are the same size, but the remote is newer,
+  # or have different sizes, but the local is newer -- are prompted for.
+  #
+  # Files are globbed on the remote host --- assuming, of course, they
+  # haven't already been globbed local, so use 'noglob' e.g. as
+  # `alias zfuget="noglob zfuget"'.
+  #
+  # Options:
+  #  -G    Glob:     turn off globbing
+  #  -v    verbose:  print more about the files listed.
+  #  -s    silent:   don't ask, just guess.  The guesses are:
+  #                - if the files have different sizes but remote is older ) grab
+  #                - if they have the same size but remote is newer        )
+  #                  which is safe if the remote files are always the right ones.
+  #   -t   time:     update the local file times to the same time as the remote.
+  #                  Currently this only works if you have the `perl' command,
+  #                  and that perl is version 5 with the standard library.
+  #                  See the function zfrtime for more gory details.
+  
+  setopt localoptions
+  unsetopt ksharrays shwordsplit
+  
+  local loc rem stat=0 locstats remstats doit tmpfile=${TMPPREFIX}zfuget$$
+  local rstat remlist verbose optlist opt bad i silent nglob time
+  
+  zfuget_print_time() {
+    local tim=$1
+    print -n "$tim[1,4]/$tim[5,6]/$tim[7,8] $tim[9,10]:$tim[11,12].$tim[13,14]"
+    print -n GMT
+  }
+  
+  zfuget_print () {
+    print -n "\nremote $rem ("
+    zfuget_print_time $remstats[2]
+    print -n ", $remstats[1] bytes)\nlocal $loc ("
+    zfuget_print_time $locstats[2]
+    print ", $locstats[1] bytes)"
+  }
+  
+  while [[ $1 = -* ]]; do
+    if [[ $1 = - || $1 = -- ]]; then
+      shift;
+      break;
+    fi
+    optlist=${1#-}
+    for (( i = 1; i <= $#optlist; i++)); do
+      opt=$optlist[$i]
+      case $optlist[$i] in
+        v) verbose=1
+  	 ;;
+        s) silent=1
+  	 ;;
+        G) nglob=1
+  	 ;;
+        t) time=1
+  	 ;;
+        *) print option $opt not recognised >&2
+  	 ;;
+      esac
+    done
+    shift
+  done
+  
+  [[ -n $bad ]] && return 1
+  
+  for remlist in $*; do
+    # zfcd directory hack to put the front back to ~
+    if [[ $remlist == $HOME || $remlist == $HOME/* ]]; then
+      remlist="~${remlist#$HOME}"
+    fi
+    if [[ $nglob != 1 ]]; then
+      zfrglob remlist
+    fi
+    if (( $#remlist )); then
+      for rem in $remlist; do
+        loc=${rem:t}
+        doit=y
+        remstats=()
+        if [[ -f $loc ]]; then
+  	zftp local $loc >$tmpfile
+  	locstats=($(<$tmpfile))
+  	zftp remote $rem >$tmpfile
+  	rstat=$?
+  	remstats=($(<$tmpfile))
+  	rm -f $tmpfile
+  	if [[ $rstat = 2 ]]; then
+  	  print "Server does not implement full command set required." 1>&2
+  	  return 1
+  	elif [[ $rstat = 1 ]]; then
+  	  print "File not found on server: $rem" 1>&2
+  	  stat=1
+  	  continue
+  	fi
+  	[[ $verbose = 1 ]] && zfuget_print
+  	if (( $locstats[1] != $remstats[1] )); then
+  	  # Files have different sizes
+  	  if [[ $locstats[2] > $remstats[2] && $silent != 1 ]]; then
+  	    [[ $verbose != 1 ]] && zfuget_print
+  	    print "Local file $loc more recent than remote," 1>&2
+  	    print -n "but sizes are different.  Transfer anyway [y/n]? " 1>&2
+  	    read -q doit
+  	  fi
+  	else
+  	  # Files have same size
+  	  if [[ $locstats[2] < $remstats[2] ]]; then
+  	    if [[ $silent != 1 ]]; then
+  	      [[ $verbose != 1 ]] && zfuget_print
+  	      print "Local file $loc has same size as remote," 1>&2
+  	      print -n "but local file is older. Transfer anyway [y/n]? " 1>&2
+  	      read -q doit
+  	    fi
+  	  else
+  	    # presumably same file, so don't get it.
+  	    [[ $verbose = 1 ]] && print Not transferring
+  	    doit=n
+  	  fi
+  	fi
+        else
+  	[[ $verbose = 1 ]] && print New file $loc
+        fi
+        if [[ $doit = y ]]; then
+  	if zftp get $rem >$loc; then
+  	  if [[ $time = 1 ]]; then
+  	    # if $remstats is set, it's second element is the remote time
+  	    zfrtime $loc $rem $remstats[2]
+  	  fi
+  	else
+  	  stat=$?
+  	fi
+  	
+        fi
+      done
+    fi
+  done
+  return $stat
+}
+
+function zfuput {
+  # Put a list of files from the server with update.
+  # See zfuget for details.
+  #
+  # Options:
+  #  -v    verbose:  print more about the files listed.
+  #  -s    silent:   don't ask, just guess.  The guesses are:
+  #                - if the files have different sizes but remote is older ) grab
+  #                - if they have the same size but remote is newer        )
+  #                  which is safe if the remote files are always the right ones.
+  
+  setopt localoptions
+  unsetopt ksharrays shwordsplit
+  
+  local loc rem stat=0 locstats remstats doit tmpfile=${TMPPREFIX}zfuput$$
+  local rstat verbose optlist opt bad i silent
+  
+  zfuput_print_time() {
+    local tim=$1
+    print -n "$tim[1,4]/$tim[5,6]/$tim[7,8] $tim[9,10]:$tim[11,12].$tim[13,14]"
+    print -n GMT
+  }
+  
+  zfuput_print () {
+    print -n "\nremote $rem ("
+    zfuput_print_time $remstats[2]
+    print -n ", $remstats[1] bytes)\nlocal $loc ("
+    zfuput_print_time $locstats[2]
+    print ", $locstats[1] bytes)"
+  }
+  
+  while [[ $1 = -* ]]; do
+    if [[ $1 = - || $1 = -- ]]; then
+      shift;
+      break;
+    fi
+    optlist=${1#-}
+    for (( i = 1; i <= $#optlist; i++)); do
+      opt=$optlist[$i]
+      case $optlist[$i] in
+        v) verbose=1
+  	 ;;
+        s) silent=1
+  	 ;;
+        *) print option $opt not recognised >&2
+  	 ;;
+      esac
+    done
+    shift
+  done
+  
+  [[ -n $bad ]] && return 1
+  
+  if [[ $ZFTP_VERBOSE = *5* ]]; then
+    # should we turn it off locally?
+    print "Messages with code 550 are harmless." >&2
+  fi
+  
+  for loc in $*; do
+    rem=${loc:t}
+    doit=y
+    remstats=()
+    if [[ ! -f $loc ]]; then
+      print "$loc: file not found" >&2
+      stat=1
+      continue
+    fi
+    zftp local $loc >$tmpfile
+    locstats=($(<$tmpfile))
+    zftp remote $rem >$tmpfile
+    rstat=$?
+    remstats=($(<$tmpfile))
+    rm -f $tmpfile
+    if [[ $rstat = 2 ]]; then
+      print "Server does not implement full command set required." 1>&2
+      return 1
+    elif [[ $rstat = 1 ]]; then
+      [[ $verbose = 1 ]] && print New file $loc
+    else
+      [[ $verbose = 1 ]] && zfuput_print
+      if (( $locstats[1] != $remstats[1] )); then
+        # Files have different sizes
+        if [[ $locstats[2] < $remstats[2] && $silent != 1 ]]; then
+  	[[ $verbose != 1 ]] && zfuput_print
+  	print "Remote file $rem more recent than local," 1>&2
+  	print -n "but sizes are different.  Transfer anyway [y/n]? " 1>&2
+  	read -q doit
+        fi
+      else
+        # Files have same size
+        if [[ $locstats[2] > $remstats[2] ]]; then
+  	if [[ $silent != 1 ]]; then
+  	  [[ $verbose != 1 ]] && zfuput_print
+  	  print "Remote file $rem has same size as local," 1>&2
+  	  print -n "but remote file is older. Transfer anyway [y/n]? " 1>&2
+  	  read -q doit
+  	fi
+        else
+  	# presumably same file, so don't get it.
+  	[[ $verbose = 1 ]] && print Not transferring
+  	doit=n
+        fi
+      fi
+    fi
+    if [[ $doit = y ]]; then
+      zftp put $rem <$loc || stat=$?
+    fi
+  done
+  return $stat
+}