]> git.lizzy.rs Git - plan9front.git/blob - sys/lib/python/ftplib.py
dist/mkfile: run binds in subshell
[plan9front.git] / sys / lib / python / ftplib.py
1 """An FTP client class and some helper functions.
2
3 Based on RFC 959: File Transfer Protocol (FTP), by J. Postel and J. Reynolds
4
5 Example:
6
7 >>> from ftplib import FTP
8 >>> ftp = FTP('ftp.python.org') # connect to host, default port
9 >>> ftp.login() # default, i.e.: user anonymous, passwd anonymous@
10 '230 Guest login ok, access restrictions apply.'
11 >>> ftp.retrlines('LIST') # list directory contents
12 total 9
13 drwxr-xr-x   8 root     wheel        1024 Jan  3  1994 .
14 drwxr-xr-x   8 root     wheel        1024 Jan  3  1994 ..
15 drwxr-xr-x   2 root     wheel        1024 Jan  3  1994 bin
16 drwxr-xr-x   2 root     wheel        1024 Jan  3  1994 etc
17 d-wxrwxr-x   2 ftp      wheel        1024 Sep  5 13:43 incoming
18 drwxr-xr-x   2 root     wheel        1024 Nov 17  1993 lib
19 drwxr-xr-x   6 1094     wheel        1024 Sep 13 19:07 pub
20 drwxr-xr-x   3 root     wheel        1024 Jan  3  1994 usr
21 -rw-r--r--   1 root     root          312 Aug  1  1994 welcome.msg
22 '226 Transfer complete.'
23 >>> ftp.quit()
24 '221 Goodbye.'
25 >>>
26
27 A nice test that reveals some of the network dialogue would be:
28 python ftplib.py -d localhost -l -p -l
29 """
30
31 #
32 # Changes and improvements suggested by Steve Majewski.
33 # Modified by Jack to work on the mac.
34 # Modified by Siebren to support docstrings and PASV.
35 #
36
37 import os
38 import sys
39
40 # Import SOCKS module if it exists, else standard socket module socket
41 try:
42     import SOCKS; socket = SOCKS; del SOCKS # import SOCKS as socket
43     from socket import getfqdn; socket.getfqdn = getfqdn; del getfqdn
44 except ImportError:
45     import socket
46
47 __all__ = ["FTP","Netrc"]
48
49 # Magic number from <socket.h>
50 MSG_OOB = 0x1                           # Process data out of band
51
52
53 # The standard FTP server control port
54 FTP_PORT = 21
55
56
57 # Exception raised when an error or invalid response is received
58 class Error(Exception): pass
59 class error_reply(Error): pass          # unexpected [123]xx reply
60 class error_temp(Error): pass           # 4xx errors
61 class error_perm(Error): pass           # 5xx errors
62 class error_proto(Error): pass          # response does not begin with [1-5]
63
64
65 # All exceptions (hopefully) that may be raised here and that aren't
66 # (always) programming errors on our side
67 all_errors = (Error, socket.error, IOError, EOFError)
68
69
70 # Line terminators (we always output CRLF, but accept any of CRLF, CR, LF)
71 CRLF = '\r\n'
72
73
74 # The class itself
75 class FTP:
76
77     '''An FTP client class.
78
79     To create a connection, call the class using these argument:
80             host, user, passwd, acct
81     These are all strings, and have default value ''.
82     Then use self.connect() with optional host and port argument.
83
84     To download a file, use ftp.retrlines('RETR ' + filename),
85     or ftp.retrbinary() with slightly different arguments.
86     To upload a file, use ftp.storlines() or ftp.storbinary(),
87     which have an open file as argument (see their definitions
88     below for details).
89     The download/upload functions first issue appropriate TYPE
90     and PORT or PASV commands.
91 '''
92
93     debugging = 0
94     host = ''
95     port = FTP_PORT
96     sock = None
97     file = None
98     welcome = None
99     passiveserver = 1
100
101     # Initialization method (called by class instantiation).
102     # Initialize host to localhost, port to standard ftp port
103     # Optional arguments are host (for connect()),
104     # and user, passwd, acct (for login())
105     def __init__(self, host='', user='', passwd='', acct=''):
106         if host:
107             self.connect(host)
108             if user: self.login(user, passwd, acct)
109
110     def connect(self, host = '', port = 0):
111         '''Connect to host.  Arguments are:
112         - host: hostname to connect to (string, default previous host)
113         - port: port to connect to (integer, default previous port)'''
114         if host: self.host = host
115         if port: self.port = port
116         msg = "getaddrinfo returns an empty list"
117         for res in socket.getaddrinfo(self.host, self.port, 0, socket.SOCK_STREAM):
118             af, socktype, proto, canonname, sa = res
119             try:
120                 self.sock = socket.socket(af, socktype, proto)
121                 self.sock.connect(sa)
122             except socket.error, msg:
123                 if self.sock:
124                     self.sock.close()
125                 self.sock = None
126                 continue
127             break
128         if not self.sock:
129             raise socket.error, msg
130         self.af = af
131         self.file = self.sock.makefile('rb')
132         self.welcome = self.getresp()
133         return self.welcome
134
135     def getwelcome(self):
136         '''Get the welcome message from the server.
137         (this is read and squirreled away by connect())'''
138         if self.debugging:
139             print '*welcome*', self.sanitize(self.welcome)
140         return self.welcome
141
142     def set_debuglevel(self, level):
143         '''Set the debugging level.
144         The required argument level means:
145         0: no debugging output (default)
146         1: print commands and responses but not body text etc.
147         2: also print raw lines read and sent before stripping CR/LF'''
148         self.debugging = level
149     debug = set_debuglevel
150
151     def set_pasv(self, val):
152         '''Use passive or active mode for data transfers.
153         With a false argument, use the normal PORT mode,
154         With a true argument, use the PASV command.'''
155         self.passiveserver = val
156
157     # Internal: "sanitize" a string for printing
158     def sanitize(self, s):
159         if s[:5] == 'pass ' or s[:5] == 'PASS ':
160             i = len(s)
161             while i > 5 and s[i-1] in '\r\n':
162                 i = i-1
163             s = s[:5] + '*'*(i-5) + s[i:]
164         return repr(s)
165
166     # Internal: send one line to the server, appending CRLF
167     def putline(self, line):
168         line = line + CRLF
169         if self.debugging > 1: print '*put*', self.sanitize(line)
170         self.sock.sendall(line)
171
172     # Internal: send one command to the server (through putline())
173     def putcmd(self, line):
174         if self.debugging: print '*cmd*', self.sanitize(line)
175         self.putline(line)
176
177     # Internal: return one line from the server, stripping CRLF.
178     # Raise EOFError if the connection is closed
179     def getline(self):
180         line = self.file.readline()
181         if self.debugging > 1:
182             print '*get*', self.sanitize(line)
183         if not line: raise EOFError
184         if line[-2:] == CRLF: line = line[:-2]
185         elif line[-1:] in CRLF: line = line[:-1]
186         return line
187
188     # Internal: get a response from the server, which may possibly
189     # consist of multiple lines.  Return a single string with no
190     # trailing CRLF.  If the response consists of multiple lines,
191     # these are separated by '\n' characters in the string
192     def getmultiline(self):
193         line = self.getline()
194         if line[3:4] == '-':
195             code = line[:3]
196             while 1:
197                 nextline = self.getline()
198                 line = line + ('\n' + nextline)
199                 if nextline[:3] == code and \
200                         nextline[3:4] != '-':
201                     break
202         return line
203
204     # Internal: get a response from the server.
205     # Raise various errors if the response indicates an error
206     def getresp(self):
207         resp = self.getmultiline()
208         if self.debugging: print '*resp*', self.sanitize(resp)
209         self.lastresp = resp[:3]
210         c = resp[:1]
211         if c in ('1', '2', '3'):
212             return resp
213         if c == '4':
214             raise error_temp, resp
215         if c == '5':
216             raise error_perm, resp
217         raise error_proto, resp
218
219     def voidresp(self):
220         """Expect a response beginning with '2'."""
221         resp = self.getresp()
222         if resp[0] != '2':
223             raise error_reply, resp
224         return resp
225
226     def abort(self):
227         '''Abort a file transfer.  Uses out-of-band data.
228         This does not follow the procedure from the RFC to send Telnet
229         IP and Synch; that doesn't seem to work with the servers I've
230         tried.  Instead, just send the ABOR command as OOB data.'''
231         line = 'ABOR' + CRLF
232         if self.debugging > 1: print '*put urgent*', self.sanitize(line)
233         self.sock.sendall(line, MSG_OOB)
234         resp = self.getmultiline()
235         if resp[:3] not in ('426', '226'):
236             raise error_proto, resp
237
238     def sendcmd(self, cmd):
239         '''Send a command and return the response.'''
240         self.putcmd(cmd)
241         return self.getresp()
242
243     def voidcmd(self, cmd):
244         """Send a command and expect a response beginning with '2'."""
245         self.putcmd(cmd)
246         return self.voidresp()
247
248     def sendport(self, host, port):
249         '''Send a PORT command with the current host and the given
250         port number.
251         '''
252         hbytes = host.split('.')
253         pbytes = [repr(port/256), repr(port%256)]
254         bytes = hbytes + pbytes
255         cmd = 'PORT ' + ','.join(bytes)
256         return self.voidcmd(cmd)
257
258     def sendeprt(self, host, port):
259         '''Send a EPRT command with the current host and the given port number.'''
260         af = 0
261         if self.af == socket.AF_INET:
262             af = 1
263         if self.af == socket.AF_INET6:
264             af = 2
265         if af == 0:
266             raise error_proto, 'unsupported address family'
267         fields = ['', repr(af), host, repr(port), '']
268         cmd = 'EPRT ' + '|'.join(fields)
269         return self.voidcmd(cmd)
270
271     def makeport(self):
272         '''Create a new socket and send a PORT command for it.'''
273         msg = "getaddrinfo returns an empty list"
274         sock = None
275         for res in socket.getaddrinfo(None, 0, self.af, socket.SOCK_STREAM, 0, socket.AI_PASSIVE):
276             af, socktype, proto, canonname, sa = res
277             try:
278                 sock = socket.socket(af, socktype, proto)
279                 sock.bind(sa)
280             except socket.error, msg:
281                 if sock:
282                     sock.close()
283                 sock = None
284                 continue
285             break
286         if not sock:
287             raise socket.error, msg
288         sock.listen(1)
289         port = sock.getsockname()[1] # Get proper port
290         host = self.sock.getsockname()[0] # Get proper host
291         if self.af == socket.AF_INET:
292             resp = self.sendport(host, port)
293         else:
294             resp = self.sendeprt(host, port)
295         return sock
296
297     def makepasv(self):
298         if self.af == socket.AF_INET:
299             host, port = parse227(self.sendcmd('PASV'))
300         else:
301             host, port = parse229(self.sendcmd('EPSV'), self.sock.getpeername())
302         return host, port
303
304     def ntransfercmd(self, cmd, rest=None):
305         """Initiate a transfer over the data connection.
306
307         If the transfer is active, send a port command and the
308         transfer command, and accept the connection.  If the server is
309         passive, send a pasv command, connect to it, and start the
310         transfer command.  Either way, return the socket for the
311         connection and the expected size of the transfer.  The
312         expected size may be None if it could not be determined.
313
314         Optional `rest' argument can be a string that is sent as the
315         argument to a RESTART command.  This is essentially a server
316         marker used to tell the server to skip over any data up to the
317         given marker.
318         """
319         size = None
320         if self.passiveserver:
321             host, port = self.makepasv()
322             af, socktype, proto, canon, sa = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM)[0]
323             conn = socket.socket(af, socktype, proto)
324             conn.connect(sa)
325             if rest is not None:
326                 self.sendcmd("REST %s" % rest)
327             resp = self.sendcmd(cmd)
328             # Some servers apparently send a 200 reply to
329             # a LIST or STOR command, before the 150 reply
330             # (and way before the 226 reply). This seems to
331             # be in violation of the protocol (which only allows
332             # 1xx or error messages for LIST), so we just discard
333             # this response.
334             if resp[0] == '2':
335                resp = self.getresp()
336             if resp[0] != '1':
337                 raise error_reply, resp
338         else:
339             sock = self.makeport()
340             if rest is not None:
341                 self.sendcmd("REST %s" % rest)
342             resp = self.sendcmd(cmd)
343             # See above.
344             if resp[0] == '2':
345                resp = self.getresp()
346             if resp[0] != '1':
347                 raise error_reply, resp
348             conn, sockaddr = sock.accept()
349         if resp[:3] == '150':
350             # this is conditional in case we received a 125
351             size = parse150(resp)
352         return conn, size
353
354     def transfercmd(self, cmd, rest=None):
355         """Like ntransfercmd() but returns only the socket."""
356         return self.ntransfercmd(cmd, rest)[0]
357
358     def login(self, user = '', passwd = '', acct = ''):
359         '''Login, default anonymous.'''
360         if not user: user = 'anonymous'
361         if not passwd: passwd = ''
362         if not acct: acct = ''
363         if user == 'anonymous' and passwd in ('', '-'):
364             # If there is no anonymous ftp password specified
365             # then we'll just use anonymous@
366             # We don't send any other thing because:
367             # - We want to remain anonymous
368             # - We want to stop SPAM
369             # - We don't want to let ftp sites to discriminate by the user,
370             #   host or country.
371             passwd = passwd + 'anonymous@'
372         resp = self.sendcmd('USER ' + user)
373         if resp[0] == '3': resp = self.sendcmd('PASS ' + passwd)
374         if resp[0] == '3': resp = self.sendcmd('ACCT ' + acct)
375         if resp[0] != '2':
376             raise error_reply, resp
377         return resp
378
379     def retrbinary(self, cmd, callback, blocksize=8192, rest=None):
380         """Retrieve data in binary mode.
381
382         `cmd' is a RETR command.  `callback' is a callback function is
383         called for each block.  No more than `blocksize' number of
384         bytes will be read from the socket.  Optional `rest' is passed
385         to transfercmd().
386
387         A new port is created for you.  Return the response code.
388         """
389         self.voidcmd('TYPE I')
390         conn = self.transfercmd(cmd, rest)
391         while 1:
392             data = conn.recv(blocksize)
393             if not data:
394                 break
395             callback(data)
396         conn.close()
397         return self.voidresp()
398
399     def retrlines(self, cmd, callback = None):
400         '''Retrieve data in line mode.
401         The argument is a RETR or LIST command.
402         The callback function (2nd argument) is called for each line,
403         with trailing CRLF stripped.  This creates a new port for you.
404         print_line() is the default callback.'''
405         if callback is None: callback = print_line
406         resp = self.sendcmd('TYPE A')
407         conn = self.transfercmd(cmd)
408         fp = conn.makefile('rb')
409         while 1:
410             line = fp.readline()
411             if self.debugging > 2: print '*retr*', repr(line)
412             if not line:
413                 break
414             if line[-2:] == CRLF:
415                 line = line[:-2]
416             elif line[-1:] == '\n':
417                 line = line[:-1]
418             callback(line)
419         fp.close()
420         conn.close()
421         return self.voidresp()
422
423     def storbinary(self, cmd, fp, blocksize=8192):
424         '''Store a file in binary mode.'''
425         self.voidcmd('TYPE I')
426         conn = self.transfercmd(cmd)
427         while 1:
428             buf = fp.read(blocksize)
429             if not buf: break
430             conn.sendall(buf)
431         conn.close()
432         return self.voidresp()
433
434     def storlines(self, cmd, fp):
435         '''Store a file in line mode.'''
436         self.voidcmd('TYPE A')
437         conn = self.transfercmd(cmd)
438         while 1:
439             buf = fp.readline()
440             if not buf: break
441             if buf[-2:] != CRLF:
442                 if buf[-1] in CRLF: buf = buf[:-1]
443                 buf = buf + CRLF
444             conn.sendall(buf)
445         conn.close()
446         return self.voidresp()
447
448     def acct(self, password):
449         '''Send new account name.'''
450         cmd = 'ACCT ' + password
451         return self.voidcmd(cmd)
452
453     def nlst(self, *args):
454         '''Return a list of files in a given directory (default the current).'''
455         cmd = 'NLST'
456         for arg in args:
457             cmd = cmd + (' ' + arg)
458         files = []
459         self.retrlines(cmd, files.append)
460         return files
461
462     def dir(self, *args):
463         '''List a directory in long form.
464         By default list current directory to stdout.
465         Optional last argument is callback function; all
466         non-empty arguments before it are concatenated to the
467         LIST command.  (This *should* only be used for a pathname.)'''
468         cmd = 'LIST'
469         func = None
470         if args[-1:] and type(args[-1]) != type(''):
471             args, func = args[:-1], args[-1]
472         for arg in args:
473             if arg:
474                 cmd = cmd + (' ' + arg)
475         self.retrlines(cmd, func)
476
477     def rename(self, fromname, toname):
478         '''Rename a file.'''
479         resp = self.sendcmd('RNFR ' + fromname)
480         if resp[0] != '3':
481             raise error_reply, resp
482         return self.voidcmd('RNTO ' + toname)
483
484     def delete(self, filename):
485         '''Delete a file.'''
486         resp = self.sendcmd('DELE ' + filename)
487         if resp[:3] in ('250', '200'):
488             return resp
489         elif resp[:1] == '5':
490             raise error_perm, resp
491         else:
492             raise error_reply, resp
493
494     def cwd(self, dirname):
495         '''Change to a directory.'''
496         if dirname == '..':
497             try:
498                 return self.voidcmd('CDUP')
499             except error_perm, msg:
500                 if msg.args[0][:3] != '500':
501                     raise
502         elif dirname == '':
503             dirname = '.'  # does nothing, but could return error
504         cmd = 'CWD ' + dirname
505         return self.voidcmd(cmd)
506
507     def size(self, filename):
508         '''Retrieve the size of a file.'''
509         # Note that the RFC doesn't say anything about 'SIZE'
510         resp = self.sendcmd('SIZE ' + filename)
511         if resp[:3] == '213':
512             s = resp[3:].strip()
513             try:
514                 return int(s)
515             except (OverflowError, ValueError):
516                 return long(s)
517
518     def mkd(self, dirname):
519         '''Make a directory, return its full pathname.'''
520         resp = self.sendcmd('MKD ' + dirname)
521         return parse257(resp)
522
523     def rmd(self, dirname):
524         '''Remove a directory.'''
525         return self.voidcmd('RMD ' + dirname)
526
527     def pwd(self):
528         '''Return current working directory.'''
529         resp = self.sendcmd('PWD')
530         return parse257(resp)
531
532     def quit(self):
533         '''Quit, and close the connection.'''
534         resp = self.voidcmd('QUIT')
535         self.close()
536         return resp
537
538     def close(self):
539         '''Close the connection without assuming anything about it.'''
540         if self.file:
541             self.file.close()
542             self.sock.close()
543             self.file = self.sock = None
544
545
546 _150_re = None
547
548 def parse150(resp):
549     '''Parse the '150' response for a RETR request.
550     Returns the expected transfer size or None; size is not guaranteed to
551     be present in the 150 message.
552     '''
553     if resp[:3] != '150':
554         raise error_reply, resp
555     global _150_re
556     if _150_re is None:
557         import re
558         _150_re = re.compile("150 .* \((\d+) bytes\)", re.IGNORECASE)
559     m = _150_re.match(resp)
560     if not m:
561         return None
562     s = m.group(1)
563     try:
564         return int(s)
565     except (OverflowError, ValueError):
566         return long(s)
567
568
569 _227_re = None
570
571 def parse227(resp):
572     '''Parse the '227' response for a PASV request.
573     Raises error_proto if it does not contain '(h1,h2,h3,h4,p1,p2)'
574     Return ('host.addr.as.numbers', port#) tuple.'''
575
576     if resp[:3] != '227':
577         raise error_reply, resp
578     global _227_re
579     if _227_re is None:
580         import re
581         _227_re = re.compile(r'(\d+),(\d+),(\d+),(\d+),(\d+),(\d+)')
582     m = _227_re.search(resp)
583     if not m:
584         raise error_proto, resp
585     numbers = m.groups()
586     host = '.'.join(numbers[:4])
587     port = (int(numbers[4]) << 8) + int(numbers[5])
588     return host, port
589
590
591 def parse229(resp, peer):
592     '''Parse the '229' response for a EPSV request.
593     Raises error_proto if it does not contain '(|||port|)'
594     Return ('host.addr.as.numbers', port#) tuple.'''
595
596     if resp[:3] != '229':
597         raise error_reply, resp
598     left = resp.find('(')
599     if left < 0: raise error_proto, resp
600     right = resp.find(')', left + 1)
601     if right < 0:
602         raise error_proto, resp # should contain '(|||port|)'
603     if resp[left + 1] != resp[right - 1]:
604         raise error_proto, resp
605     parts = resp[left + 1:right].split(resp[left+1])
606     if len(parts) != 5:
607         raise error_proto, resp
608     host = peer[0]
609     port = int(parts[3])
610     return host, port
611
612
613 def parse257(resp):
614     '''Parse the '257' response for a MKD or PWD request.
615     This is a response to a MKD or PWD request: a directory name.
616     Returns the directoryname in the 257 reply.'''
617
618     if resp[:3] != '257':
619         raise error_reply, resp
620     if resp[3:5] != ' "':
621         return '' # Not compliant to RFC 959, but UNIX ftpd does this
622     dirname = ''
623     i = 5
624     n = len(resp)
625     while i < n:
626         c = resp[i]
627         i = i+1
628         if c == '"':
629             if i >= n or resp[i] != '"':
630                 break
631             i = i+1
632         dirname = dirname + c
633     return dirname
634
635
636 def print_line(line):
637     '''Default retrlines callback to print a line.'''
638     print line
639
640
641 def ftpcp(source, sourcename, target, targetname = '', type = 'I'):
642     '''Copy file from one FTP-instance to another.'''
643     if not targetname: targetname = sourcename
644     type = 'TYPE ' + type
645     source.voidcmd(type)
646     target.voidcmd(type)
647     sourcehost, sourceport = parse227(source.sendcmd('PASV'))
648     target.sendport(sourcehost, sourceport)
649     # RFC 959: the user must "listen" [...] BEFORE sending the
650     # transfer request.
651     # So: STOR before RETR, because here the target is a "user".
652     treply = target.sendcmd('STOR ' + targetname)
653     if treply[:3] not in ('125', '150'): raise error_proto  # RFC 959
654     sreply = source.sendcmd('RETR ' + sourcename)
655     if sreply[:3] not in ('125', '150'): raise error_proto  # RFC 959
656     source.voidresp()
657     target.voidresp()
658
659
660 class Netrc:
661     """Class to parse & provide access to 'netrc' format files.
662
663     See the netrc(4) man page for information on the file format.
664
665     WARNING: This class is obsolete -- use module netrc instead.
666
667     """
668     __defuser = None
669     __defpasswd = None
670     __defacct = None
671
672     def __init__(self, filename=None):
673         if filename is None:
674             if "HOME" in os.environ:
675                 filename = os.path.join(os.environ["HOME"],
676                                         ".netrc")
677             else:
678                 raise IOError, \
679                       "specify file to load or set $HOME"
680         self.__hosts = {}
681         self.__macros = {}
682         fp = open(filename, "r")
683         in_macro = 0
684         while 1:
685             line = fp.readline()
686             if not line: break
687             if in_macro and line.strip():
688                 macro_lines.append(line)
689                 continue
690             elif in_macro:
691                 self.__macros[macro_name] = tuple(macro_lines)
692                 in_macro = 0
693             words = line.split()
694             host = user = passwd = acct = None
695             default = 0
696             i = 0
697             while i < len(words):
698                 w1 = words[i]
699                 if i+1 < len(words):
700                     w2 = words[i + 1]
701                 else:
702                     w2 = None
703                 if w1 == 'default':
704                     default = 1
705                 elif w1 == 'machine' and w2:
706                     host = w2.lower()
707                     i = i + 1
708                 elif w1 == 'login' and w2:
709                     user = w2
710                     i = i + 1
711                 elif w1 == 'password' and w2:
712                     passwd = w2
713                     i = i + 1
714                 elif w1 == 'account' and w2:
715                     acct = w2
716                     i = i + 1
717                 elif w1 == 'macdef' and w2:
718                     macro_name = w2
719                     macro_lines = []
720                     in_macro = 1
721                     break
722                 i = i + 1
723             if default:
724                 self.__defuser = user or self.__defuser
725                 self.__defpasswd = passwd or self.__defpasswd
726                 self.__defacct = acct or self.__defacct
727             if host:
728                 if host in self.__hosts:
729                     ouser, opasswd, oacct = \
730                            self.__hosts[host]
731                     user = user or ouser
732                     passwd = passwd or opasswd
733                     acct = acct or oacct
734                 self.__hosts[host] = user, passwd, acct
735         fp.close()
736
737     def get_hosts(self):
738         """Return a list of hosts mentioned in the .netrc file."""
739         return self.__hosts.keys()
740
741     def get_account(self, host):
742         """Returns login information for the named host.
743
744         The return value is a triple containing userid,
745         password, and the accounting field.
746
747         """
748         host = host.lower()
749         user = passwd = acct = None
750         if host in self.__hosts:
751             user, passwd, acct = self.__hosts[host]
752         user = user or self.__defuser
753         passwd = passwd or self.__defpasswd
754         acct = acct or self.__defacct
755         return user, passwd, acct
756
757     def get_macros(self):
758         """Return a list of all defined macro names."""
759         return self.__macros.keys()
760
761     def get_macro(self, macro):
762         """Return a sequence of lines which define a named macro."""
763         return self.__macros[macro]
764
765
766
767 def test():
768     '''Test program.
769     Usage: ftp [-d] [-r[file]] host [-l[dir]] [-d[dir]] [-p] [file] ...
770
771     -d dir
772     -l list
773     -p password
774     '''
775
776     if len(sys.argv) < 2:
777         print test.__doc__
778         sys.exit(0)
779
780     debugging = 0
781     rcfile = None
782     while sys.argv[1] == '-d':
783         debugging = debugging+1
784         del sys.argv[1]
785     if sys.argv[1][:2] == '-r':
786         # get name of alternate ~/.netrc file:
787         rcfile = sys.argv[1][2:]
788         del sys.argv[1]
789     host = sys.argv[1]
790     ftp = FTP(host)
791     ftp.set_debuglevel(debugging)
792     userid = passwd = acct = ''
793     try:
794         netrc = Netrc(rcfile)
795     except IOError:
796         if rcfile is not None:
797             sys.stderr.write("Could not open account file"
798                              " -- using anonymous login.")
799     else:
800         try:
801             userid, passwd, acct = netrc.get_account(host)
802         except KeyError:
803             # no account for host
804             sys.stderr.write(
805                     "No account -- using anonymous login.")
806     ftp.login(userid, passwd, acct)
807     for file in sys.argv[2:]:
808         if file[:2] == '-l':
809             ftp.dir(file[2:])
810         elif file[:2] == '-d':
811             cmd = 'CWD'
812             if file[2:]: cmd = cmd + ' ' + file[2:]
813             resp = ftp.sendcmd(cmd)
814         elif file == '-p':
815             ftp.set_pasv(not ftp.passiveserver)
816         else:
817             ftp.retrbinary('RETR ' + file, \
818                            sys.stdout.write, 1024)
819     ftp.quit()
820
821
822 if __name__ == '__main__':
823     test()