| #! /usr/bin/env python | 
 |  | 
 | # A simple gopher client. | 
 | # | 
 | # Usage: gopher [ [selector] host [port] ] | 
 |  | 
 | import sys | 
 | import os | 
 | import socket | 
 |  | 
 | # Default selector, host and port | 
 | DEF_SELECTOR = '' | 
 | DEF_HOST     = 'gopher.micro.umn.edu' | 
 | DEF_PORT     = 70 | 
 |  | 
 | # Recognized file types | 
 | T_TEXTFILE  = '0' | 
 | T_MENU      = '1' | 
 | T_CSO       = '2' | 
 | T_ERROR     = '3' | 
 | T_BINHEX    = '4' | 
 | T_DOS       = '5' | 
 | T_UUENCODE  = '6' | 
 | T_SEARCH    = '7' | 
 | T_TELNET    = '8' | 
 | T_BINARY    = '9' | 
 | T_REDUNDANT = '+' | 
 | T_SOUND     = 's' | 
 |  | 
 | # Dictionary mapping types to strings | 
 | typename = {'0': '<TEXT>', '1': '<DIR>', '2': '<CSO>', '3': '<ERROR>', \ | 
 |         '4': '<BINHEX>', '5': '<DOS>', '6': '<UUENCODE>', '7': '<SEARCH>', \ | 
 |         '8': '<TELNET>', '9': '<BINARY>', '+': '<REDUNDANT>', 's': '<SOUND>'} | 
 |  | 
 | # Oft-used characters and strings | 
 | CRLF = '\r\n' | 
 | TAB = '\t' | 
 |  | 
 | # Open a TCP connection to a given host and port | 
 | def open_socket(host, port): | 
 |     if not port: | 
 |         port = DEF_PORT | 
 |     elif type(port) == type(''): | 
 |         port = int(port) | 
 |     s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | 
 |     s.connect((host, port)) | 
 |     return s | 
 |  | 
 | # Send a selector to a given host and port, return a file with the reply | 
 | def send_request(selector, host, port): | 
 |     s = open_socket(host, port) | 
 |     s.send(selector + CRLF) | 
 |     s.shutdown(1) | 
 |     return s.makefile('r') | 
 |  | 
 | # Get a menu in the form of a list of entries | 
 | def get_menu(selector, host, port): | 
 |     f = send_request(selector, host, port) | 
 |     list = [] | 
 |     while 1: | 
 |         line = f.readline() | 
 |         if not line: | 
 |             print('(Unexpected EOF from server)') | 
 |             break | 
 |         if line[-2:] == CRLF: | 
 |             line = line[:-2] | 
 |         elif line[-1:] in CRLF: | 
 |             line = line[:-1] | 
 |         if line == '.': | 
 |             break | 
 |         if not line: | 
 |             print('(Empty line from server)') | 
 |             continue | 
 |         typechar = line[0] | 
 |         parts = line[1:].split(TAB) | 
 |         if len(parts) < 4: | 
 |             print('(Bad line from server: %r)' % (line,)) | 
 |             continue | 
 |         if len(parts) > 4: | 
 |             print('(Extra info from server: %r)' % (parts[4:],)) | 
 |         parts.insert(0, typechar) | 
 |         list.append(parts) | 
 |     f.close() | 
 |     return list | 
 |  | 
 | # Get a text file as a list of lines, with trailing CRLF stripped | 
 | def get_textfile(selector, host, port): | 
 |     list = [] | 
 |     get_alt_textfile(selector, host, port, list.append) | 
 |     return list | 
 |  | 
 | # Get a text file and pass each line to a function, with trailing CRLF stripped | 
 | def get_alt_textfile(selector, host, port, func): | 
 |     f = send_request(selector, host, port) | 
 |     while 1: | 
 |         line = f.readline() | 
 |         if not line: | 
 |             print('(Unexpected EOF from server)') | 
 |             break | 
 |         if line[-2:] == CRLF: | 
 |             line = line[:-2] | 
 |         elif line[-1:] in CRLF: | 
 |             line = line[:-1] | 
 |         if line == '.': | 
 |             break | 
 |         if line[:2] == '..': | 
 |             line = line[1:] | 
 |         func(line) | 
 |     f.close() | 
 |  | 
 | # Get a binary file as one solid data block | 
 | def get_binary(selector, host, port): | 
 |     f = send_request(selector, host, port) | 
 |     data = f.read() | 
 |     f.close() | 
 |     return data | 
 |  | 
 | # Get a binary file and pass each block to a function | 
 | def get_alt_binary(selector, host, port, func, blocksize): | 
 |     f = send_request(selector, host, port) | 
 |     while 1: | 
 |         data = f.read(blocksize) | 
 |         if not data: | 
 |             break | 
 |         func(data) | 
 |  | 
 | # A *very* simple interactive browser | 
 |  | 
 | # Browser main command, has default arguments | 
 | def browser(*args): | 
 |     selector = DEF_SELECTOR | 
 |     host = DEF_HOST | 
 |     port = DEF_PORT | 
 |     n = len(args) | 
 |     if n > 0 and args[0]: | 
 |         selector = args[0] | 
 |     if n > 1 and args[1]: | 
 |         host = args[1] | 
 |     if n > 2 and args[2]: | 
 |         port = args[2] | 
 |     if n > 3: | 
 |         raise RuntimeError('too many args') | 
 |     try: | 
 |         browse_menu(selector, host, port) | 
 |     except socket.error as msg: | 
 |         print('Socket error:', msg) | 
 |         sys.exit(1) | 
 |     except KeyboardInterrupt: | 
 |         print('\n[Goodbye]') | 
 |  | 
 | # Browse a menu | 
 | def browse_menu(selector, host, port): | 
 |     list = get_menu(selector, host, port) | 
 |     while 1: | 
 |         print('----- MENU -----') | 
 |         print('Selector:', repr(selector)) | 
 |         print('Host:', host, ' Port:', port) | 
 |         print() | 
 |         for i in range(len(list)): | 
 |             item = list[i] | 
 |             typechar, description = item[0], item[1] | 
 |             print(repr(i+1).rjust(3) + ':', description, end=' ') | 
 |             if typechar in typename: | 
 |                 print(typename[typechar]) | 
 |             else: | 
 |                 print('<TYPE=' + repr(typechar) + '>') | 
 |         print() | 
 |         while 1: | 
 |             try: | 
 |                 str = input('Choice [CR == up a level]: ') | 
 |             except EOFError: | 
 |                 print() | 
 |                 return | 
 |             if not str: | 
 |                 return | 
 |             try: | 
 |                 choice = int(str) | 
 |             except ValueError: | 
 |                 print('Choice must be a number; try again:') | 
 |                 continue | 
 |             if not 0 < choice <= len(list): | 
 |                 print('Choice out of range; try again:') | 
 |                 continue | 
 |             break | 
 |         item = list[choice-1] | 
 |         typechar = item[0] | 
 |         [i_selector, i_host, i_port] = item[2:5] | 
 |         if typechar in typebrowser: | 
 |             browserfunc = typebrowser[typechar] | 
 |             try: | 
 |                 browserfunc(i_selector, i_host, i_port) | 
 |             except (IOError, socket.error): | 
 |                 t, v, tb = sys.exc_info() | 
 |                 print('***', t, ':', v) | 
 |         else: | 
 |             print('Unsupported object type') | 
 |  | 
 | # Browse a text file | 
 | def browse_textfile(selector, host, port): | 
 |     x = None | 
 |     try: | 
 |         p = os.popen('${PAGER-more}', 'w') | 
 |         x = SaveLines(p) | 
 |         get_alt_textfile(selector, host, port, x.writeln) | 
 |     except IOError as msg: | 
 |         print('IOError:', msg) | 
 |     if x: | 
 |         x.close() | 
 |     f = open_savefile() | 
 |     if not f: | 
 |         return | 
 |     x = SaveLines(f) | 
 |     try: | 
 |         get_alt_textfile(selector, host, port, x.writeln) | 
 |         print('Done.') | 
 |     except IOError as msg: | 
 |         print('IOError:', msg) | 
 |     x.close() | 
 |  | 
 | def raw_input(prompt): | 
 |     sys.stdout.write(prompt) | 
 |     sys.stdout.flush() | 
 |     return sys.stdin.readline() | 
 |  | 
 | # Browse a search index | 
 | def browse_search(selector, host, port): | 
 |     while 1: | 
 |         print('----- SEARCH -----') | 
 |         print('Selector:', repr(selector)) | 
 |         print('Host:', host, ' Port:', port) | 
 |         print() | 
 |         try: | 
 |             query = input('Query [CR == up a level]: ') | 
 |         except EOFError: | 
 |             print() | 
 |             break | 
 |         query = query.strip() | 
 |         if not query: | 
 |             break | 
 |         if '\t' in query: | 
 |             print('Sorry, queries cannot contain tabs') | 
 |             continue | 
 |         browse_menu(selector + TAB + query, host, port) | 
 |  | 
 | # "Browse" telnet-based information, i.e. open a telnet session | 
 | def browse_telnet(selector, host, port): | 
 |     if selector: | 
 |         print('Log in as', repr(selector)) | 
 |     if type(port) != type(''): | 
 |         port = repr(port) | 
 |     sts = os.system('set -x; exec telnet ' + host + ' ' + port) | 
 |     if sts: | 
 |         print('Exit status:', sts) | 
 |  | 
 | # "Browse" a binary file, i.e. save it to a file | 
 | def browse_binary(selector, host, port): | 
 |     f = open_savefile() | 
 |     if not f: | 
 |         return | 
 |     x = SaveWithProgress(f) | 
 |     get_alt_binary(selector, host, port, x.write, 8*1024) | 
 |     x.close() | 
 |  | 
 | # "Browse" a sound file, i.e. play it or save it | 
 | def browse_sound(selector, host, port): | 
 |     browse_binary(selector, host, port) | 
 |  | 
 | # Dictionary mapping types to browser functions | 
 | typebrowser = {'0': browse_textfile, '1': browse_menu, \ | 
 |         '4': browse_binary, '5': browse_binary, '6': browse_textfile, \ | 
 |         '7': browse_search, \ | 
 |         '8': browse_telnet, '9': browse_binary, 's': browse_sound} | 
 |  | 
 | # Class used to save lines, appending a newline to each line | 
 | class SaveLines: | 
 |     def __init__(self, f): | 
 |         self.f = f | 
 |     def writeln(self, line): | 
 |         self.f.write(line + '\n') | 
 |     def close(self): | 
 |         sts = self.f.close() | 
 |         if sts: | 
 |             print('Exit status:', sts) | 
 |  | 
 | # Class used to save data while showing progress | 
 | class SaveWithProgress: | 
 |     def __init__(self, f): | 
 |         self.f = f | 
 |     def write(self, data): | 
 |         sys.stdout.write('#') | 
 |         sys.stdout.flush() | 
 |         self.f.write(data) | 
 |     def close(self): | 
 |         print() | 
 |         sts = self.f.close() | 
 |         if sts: | 
 |             print('Exit status:', sts) | 
 |  | 
 | # Ask for and open a save file, or return None if not to save | 
 | def open_savefile(): | 
 |     try: | 
 |         savefile = input( \ | 
 |     'Save as file [CR == don\'t save; |pipeline or ~user/... OK]: ') | 
 |     except EOFError: | 
 |         print() | 
 |         return None | 
 |     savefile = savefile.strip() | 
 |     if not savefile: | 
 |         return None | 
 |     if savefile[0] == '|': | 
 |         cmd = savefile[1:].strip() | 
 |         try: | 
 |             p = os.popen(cmd, 'w') | 
 |         except IOError as msg: | 
 |             print(repr(cmd), ':', msg) | 
 |             return None | 
 |         print('Piping through', repr(cmd), '...') | 
 |         return p | 
 |     if savefile[0] == '~': | 
 |         savefile = os.path.expanduser(savefile) | 
 |     try: | 
 |         f = open(savefile, 'w') | 
 |     except IOError as msg: | 
 |         print(repr(savefile), ':', msg) | 
 |         return None | 
 |     print('Saving to', repr(savefile), '...') | 
 |     return f | 
 |  | 
 | # Test program | 
 | def test(): | 
 |     if sys.argv[4:]: | 
 |         print('usage: gopher [ [selector] host [port] ]') | 
 |         sys.exit(2) | 
 |     elif sys.argv[3:]: | 
 |         browser(sys.argv[1], sys.argv[2], sys.argv[3]) | 
 |     elif sys.argv[2:]: | 
 |         try: | 
 |             port = int(sys.argv[2]) | 
 |             selector = '' | 
 |             host = sys.argv[1] | 
 |         except ValueError: | 
 |             selector = sys.argv[1] | 
 |             host = sys.argv[2] | 
 |             port = '' | 
 |         browser(selector, host, port) | 
 |     elif sys.argv[1:]: | 
 |         browser('', sys.argv[1]) | 
 |     else: | 
 |         browser() | 
 |  | 
 | # Call the test program as a main program | 
 | test() |