1
0
Fork 0
mirror of git://git.code.sf.net/p/zsh/code synced 2025-01-19 23:41:31 +01:00
zsh/Misc/zftp-functions
1999-04-15 18:16:27 +00:00

1510 lines
44 KiB
Text

# 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
}