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