changeset 185:b2683d01a72f

terminator problem in cat
author catherine@Elli.myhome.westell.com
date Thu, 13 Nov 2008 14:54:07 -0500
parents 2ab3a24d7974
children e26523dd0aeb
files sqlpython/sqlpyPlus.py
diffstat 1 files changed, 975 insertions(+), 987 deletions(-) [+]
line wrap: on
line diff
--- a/sqlpython/sqlpyPlus.py	Fri Nov 07 16:14:21 2008 -0500
+++ b/sqlpython/sqlpyPlus.py	Thu Nov 13 14:54:07 2008 -0500
@@ -1,988 +1,976 @@
-"""sqlpyPlus - extra features (inspired	 by Oracle SQL*Plus) for Luca Canali's sqlpython.py
-
-Features include:
- - SQL*Plus-style bind variables
- - `set autobind on` stores single-line result sets in bind variables automatically
- - SQL buffer with list, run, ed, get, etc.; unlike SQL*Plus, buffer stores session's full history
- - @script.sql loads and runs (like SQL*Plus)
- - ! runs operating-system command
- - show and set to control sqlpython parameters
- - SQL*Plus-style describe, spool
- - write sends query result directly to file
- - comments shows table and column comments
- - compare ... to ... graphically compares results of two queries
- - commands are case-insensitive
- - context-sensitive tab-completion for table names, column names, etc.
-
-Use 'help' within sqlpython for details.
-
-Set bind variables the hard (SQL*Plus) way
-exec :b = 3
-or with a python-like shorthand
-:b = 3
-
-- catherinedevlin.blogspot.com  May 31, 2006
-"""
-import sys, os, re, sqlpython, cx_Oracle, pyparsing, re, completion, datetime, pickle
-from cmd2 import Cmd, make_option, options, Statekeeper, Cmd2TestCase
-from output_templates import output_templates
-from plothandler import Plot
-try:
-    import pylab
-except:
-    pass
-
-descQueries = {
-'TABLE': ("""
-          atc.column_name,
-CASE atc.nullable WHEN 'Y' THEN 'NULL' ELSE 'NOT NULL' END "Null?",
-atc.data_type ||
-CASE atc.data_type WHEN 'DATE' THEN ''
-ELSE '(' ||
-CASE atc.data_type WHEN 'NUMBER' THEN TO_CHAR(atc.data_precision) ||
-CASE atc.data_scale WHEN 0 THEN ''
-ELSE ',' || TO_CHAR(atc.data_scale) END
-ELSE TO_CHAR(atc.data_length) END 
-END ||
-CASE atc.data_type WHEN 'DATE' THEN '' ELSE ')' END
-data_type
-FROM all_tab_columns atc
-WHERE atc.table_name = :object_name
-AND      atc.owner = :owner
-ORDER BY atc.column_id;""",),
-'PROCEDURE': ("""
-              NVL(argument_name, 'Return Value') argument_name,             
-data_type,
-in_out,
-default_value
-FROM all_arguments
-WHERE object_name = :object_name
-AND      owner = :owner
-AND      package_name IS NULL
-ORDER BY sequence;""",),    
-'PackageObjects':("""
-SELECT DISTINCT object_name
-FROM all_arguments
-WHERE package_name = :package_name
-AND      owner = :owner""",),
-'PackageObjArgs':("""
-                  object_name,
-argument_name,             
-data_type,
-in_out,
-default_value
-FROM all_arguments
-WHERE package_name = :package_name
-AND      object_name = :object_name
-AND      owner = :owner
-AND      argument_name IS NOT NULL
-ORDER BY sequence""",),
-'TRIGGER':("""
-           description
-FROM   all_triggers
-WHERE  owner = :owner
-AND    trigger_name = :object_name
-""",
-"""
-table_owner,
-base_object_type,
-table_name,
-column_name,
-when_clause,
-status,
-action_type,
-crossedition
-FROM   all_triggers
-WHERE  owner = :owner
-AND    trigger_name = :object_name
-\\t
-""",
-),
-'INDEX':("""
-index_type,
-table_owner,
-table_name,
-table_type,
-uniqueness,
-compression,
-partitioned,
-temporary,
-generated,
-secondary,
-dropped,
-visibility
-FROM   all_indexes
-WHERE  owner = :owner
-AND    index_name = :object_name
-\\t
-""",)
-}
-descQueries['VIEW'] = descQueries['TABLE']
-descQueries['FUNCTION'] = descQueries['PROCEDURE'] 
-
-queries = {
-'resolve': """
-SELECT object_type, object_name, owner FROM (
-SELECT object_type, object_name, user owner, 1 priority
-FROM   user_objects
-WHERE object_name = :objName
-UNION ALL
-SELECT ao.object_type, ao.object_name, ao.owner, 2 priority
-FROM    all_objects ao
-JOIN      user_synonyms us ON (us.table_owner = ao.owner AND us.table_name = ao.object_name)
-WHERE us.synonym_name = :objName
-AND   ao.object_type != 'SYNONYM'
-UNION ALL
-SELECT ao.object_type, ao.object_name, ao.owner, 3 priority
-FROM    all_objects ao
-JOIN      all_synonyms asyn ON (asyn.table_owner = ao.owner AND asyn.table_name = ao.object_name)
-WHERE asyn.synonym_name = :objName
-AND   ao.object_type != 'SYNONYM'
-AND      asyn.owner = 'PUBLIC'
-UNION ALL 
-SELECT 'DIRECTORY' object_type, dir.directory_name, dir.owner, 6 priority
-FROM   all_directories dir
-WHERE  dir.directory_name = :objName
-UNION ALL 
-SELECT 'DATABASE LINK' object_type, db_link, owner, 7 priority
-FROM   all_db_links dbl
-WHERE  dbl.db_link = :objName
-) ORDER BY priority ASC,
-           length(object_type) ASC,
-           object_type DESC""", # preference: PACKAGE before PACKAGE BODY, TABLE before INDEX
-'tabComments': """
-SELECT comments
-FROM    all_tab_comments
-WHERE owner = :owner
-AND      table_name = :table_name""",
-'colComments': """
-atc.column_name,
-acc.comments             
-FROM all_tab_columns atc
-JOIN all_col_comments acc ON (atc.owner = acc.owner and atc.table_name = acc.table_name and atc.column_name = acc.column_name)
-WHERE atc.table_name = :object_name
-AND      atc.owner = :owner
-ORDER BY atc.column_id;""",
-'oneColComments': """
-atc.column_name,
-acc.comments             
-FROM all_tab_columns atc
-JOIN all_col_comments acc ON (atc.owner = acc.owner and atc.table_name = acc.table_name and atc.column_name = acc.column_name)
-WHERE atc.table_name = :object_name
-AND      atc.owner = :owner
-AND      acc.column_name = :column_name
-ORDER BY atc.column_id;""",
-#thanks to Senora.pm for "refs"
-'refs': """
-NULL               referenced_by, 
-c2.table_name      references, 
-c1.constraint_name constraint
-FROM
-user_constraints c1,
-user_constraints c2
-WHERE
-c1.table_name = :object_name
-and c1.constraint_type ='R'
-and c1.r_constraint_name = c2.constraint_name
-and c1.r_owner = c2.owner
-and c1.owner = :owner
-UNION
-SELECT c1.table_name      referenced_by, 
-NULL               references, 
-c1.constraint_name constraint
-FROM
-user_constraints c1,
-user_constraints c2
-WHERE
-c2.table_name = :object_name
-and c1.constraint_type ='R'
-and c1.r_constraint_name = c2.constraint_name
-and c1.r_owner = c2.owner
-and c1.owner = :owner       
-"""
-}
-
-if float(sys.version[:3]) < 2.3:
-    def enumerate(lst):
-        return zip(range(len(lst)), lst)
-
-class SoftwareSearcher(object):
-    def __init__(self, softwareList, purpose):
-        self.softwareList = softwareList
-        self.purpose = purpose
-        self.software = None
-    def invoke(self, *args):
-        if not self.software:
-            (self.software, self.invokeString) = self.find()            
-        argTuple = tuple([self.software] + list(args))
-        os.system(self.invokeString % argTuple)
-    def find(self):
-        if self.purpose == 'text editor':
-            software = os.environ.get('EDITOR')
-            if software:
-                return (software, '%s %s')
-        for (n, (software, invokeString)) in enumerate(self.softwareList):
-            if os.path.exists(software):
-                if n > (len(self.softwareList) * 0.7):
-                    print """
-
-                          Using %s.  Note that there are better options available for %s,
-                          but %s couldn't find a better one in your PATH.
-                          Feel free to open up %s
-                          and customize it to find your favorite %s program.
-
-                          """ % (software, self.purpose, __file__, __file__, self.purpose)
-                return (software, invokeString)
-            stem = os.path.split(software)[1]
-            for p in os.environ['PATH'].split(os.pathsep):
-                if os.path.exists(os.sep.join([p, stem])):
-                    return (stem, invokeString)
-        raise (OSError, """Could not find any %s programs.  You will need to install one,
-               or customize %s to make it aware of yours.
-Looked for these programs:
-%s""" % (self.purpose, __file__, "\n".join([s[0] for s in self.softwareList])))
-    #v2.4: %s""" % (self.purpose, __file__, "\n".join(s[0] for s in self.softwareList)))
-
-softwareLists = {
-    'diff/merge': [  
-        ('/usr/bin/meld',"%s %s %s"),
-        ('/usr/bin/kdiff3',"%s %s %s"),
-        (r'C:\Program Files\Araxis\Araxis Merge v6.5\Merge.exe','"%s" %s %s'),                
-        (r'C:\Program Files\TortoiseSVN\bin\TortoiseMerge.exe', '"%s" /base:"%s" /mine:"%s"'),
-        ('FileMerge','%s %s %s'),        
-        ('kompare','%s %s %s'),   
-        ('WinMerge','%s %s %s'),         
-        ('xxdiff','%s %s %s'),        
-        ('fldiff','%s %s %s'),
-        ('gtkdiff','%s %s %s'),        
-        ('tkdiff','%s %s %s'),         
-        ('gvimdiff','%s %s %s'),        
-        ('diff',"%s %s %s"),
-        (r'c:\windows\system32\comp.exe',"%s %s %s")],
-        'text editor': [
-            ('gedit', '%s %s'),
-            ('textpad', '%s %s'),
-            ('notepad.exe', '%s %s'),
-            ('pico', '%s %s'),
-            ('emacs', '%s %s'),
-            ('vim', '%s %s'),
-            ('vi', '%s %s'),
-            ('ed', '%s %s'),
-            ('edlin', '%s %s')
-        ]
-}
-
-diffMergeSearcher = SoftwareSearcher(softwareLists['diff/merge'],'diff/merge')
-editSearcher = SoftwareSearcher(softwareLists['text editor'], 'text editor')
-editor = os.environ.get('EDITOR')
-if editor:
-    editSearcher.find = lambda: (editor, "%s %s")
-
-class CaselessDict(dict):
-    """dict with case-insensitive keys.
-
-    Posted to ASPN Python Cookbook by Jeff Donner - http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66315"""
-    def __init__(self, other=None):
-        if other:
-            # Doesn't do keyword args
-            if isinstance(other, dict):
-                for k,v in other.items():
-                    dict.__setitem__(self, k.lower(), v)
-            else:
-                for k,v in other:
-                    dict.__setitem__(self, k.lower(), v)
-    def __getitem__(self, key):
-        return dict.__getitem__(self, key.lower())
-    def __setitem__(self, key, value):
-        dict.__setitem__(self, key.lower(), value)
-    def __contains__(self, key):
-        return dict.__contains__(self, key.lower())
-    def has_key(self, key):
-        return dict.has_key(self, key.lower())
-    def get(self, key, def_val=None):
-        return dict.get(self, key.lower(), def_val)
-    def setdefault(self, key, def_val=None):
-        return dict.setdefault(self, key.lower(), def_val)
-    def update(self, other):
-        for k,v in other.items():
-            dict.__setitem__(self, k.lower(), v)
-    def fromkeys(self, iterable, value=None):
-        d = CaselessDict()
-        for k in iterable:
-            dict.__setitem__(d, k.lower(), value)
-        return d
-    def pop(self, key, def_val=None):
-        return dict.pop(self, key.lower(), def_val)
-
-class Parser(object):
-    comment_def = "--" + pyparsing.ZeroOrMore(pyparsing.CharsNotIn("\n"))    
-    def __init__(self, scanner, retainSeparator=True):
-        self.scanner = scanner
-        self.scanner.ignore(pyparsing.sglQuotedString)
-        self.scanner.ignore(pyparsing.dblQuotedString)
-        self.scanner.ignore(self.comment_def)
-        self.scanner.ignore(pyparsing.cStyleComment)
-        self.retainSeparator = retainSeparator
-    def separate(self, txt):
-        itms = []
-        for (sqlcommand, start, end) in self.scanner.scanString(txt):
-            if sqlcommand:
-                if type(sqlcommand[0]) == pyparsing.ParseResults:
-                    if self.retainSeparator:
-                        itms.append("".join(sqlcommand[0]))
-                    else:
-                        itms.append(sqlcommand[0][0])
-                else:
-                    if sqlcommand[0]:
-                        itms.append(sqlcommand[0])
-        return itms
-
-bindScanner = Parser(pyparsing.Literal(':') + pyparsing.Word( pyparsing.alphanums + "_$#" ))   
-    
-def findBinds(target, existingBinds, givenBindVars = {}):
-    result = givenBindVars
-    for finding, startat, endat in bindScanner.scanner.scanString(target):
-        varname = finding[1]
-        try:
-            result[varname] = existingBinds[varname]
-        except KeyError:
-            if not givenBindVars.has_key(varname):
-                print 'Bind variable %s not defined.' % (varname)                
-    return result
-       
-class sqlpyPlus(sqlpython.sqlpython):
-    defaultExtension = 'sql'
-    sqlpython.sqlpython.shortcuts.update({':': 'setbind', 
-                                          '\\': 'psql', 
-                                          '@': '_load',
-                                          '--': '_comment',
-                                          '/*': '_multiline_comment'})
-    multilineCommands = '''select insert update delete tselect
-                      create drop alter _multiline_comment'''.split()
-    sqlpython.sqlpython.noSpecialParse.append('spool')
-    defaultFileName = 'afiedt.buf'
-    def __init__(self):
-        sqlpython.sqlpython.__init__(self)
-        self.binds = CaselessDict()
-        self.sqlBuffer = []
-        self.settable = 'autobind commit_on_exit echo maxfetch maxtselctrows timeout'.split()
-        # settables must be lowercase
-        self.stdoutBeforeSpool = sys.stdout
-        self.spoolFile = None
-        self.autobind = False
-    def default(self, arg):
-        sqlpython.sqlpython.default(self, arg)
-        self.sqlBuffer.append(self.query)            
-
-    # overrides cmd's parseline
-    def parseline(self, line):
-        """Parse the line into a command name and a string containing
-        the arguments.  Returns a tuple containing (command, args, line).
-        'command' and 'args' may be None if the line couldn't be parsed.        
-        Overrides cmd.cmd.parseline to accept variety of shortcuts.."""
-
-        cmd, arg, line = sqlpython.sqlpython.parseline(self, line)
-        if cmd in ('select', 'sleect', 'insert', 'update', 'delete', 'describe',
-                   'desc', 'comments', 'pull', 'refs', 'desc', 'triggers', 'find') \
-           and not hasattr(self, 'curs'):
-            print 'Not connected.'
-            return '', '', ''
-        return cmd, arg, line
-    
-    do__load = Cmd.do_load
-    
-    def do__comment(self, arg):
-        pass
-    def do__multiline_comment(self, arg):
-        pass
-
-    def onecmd_plus_hooks(self, line):                          
-        line = self.precmd(line)
-        stop = self.onecmd(line)
-        stop = self.postcmd(stop, line)
-
-    def do_shortcuts(self,arg):
-        """Lists available first-character shortcuts
-        (i.e. '!dir' is equivalent to 'shell dir')"""
-        for (scchar, scto) in self.shortcuts.items():
-            print '%s: %s' % (scchar, scto)
-
-    def colnames(self):
-        return [d[0] for d in curs.description]
-
-    def sql_format_itm(self, itm, needsquotes):
-        if itm is None:
-            return 'NULL'
-        if needsquotes:
-            return "'%s'" % str(itm)
-        return str(itm)
-    tableNameFinder = re.compile(r'from\s+([\w$#_"]+)', re.IGNORECASE | re.MULTILINE | re.DOTALL)          
-    inputStatementFormatters = {
-        cx_Oracle.STRING: "'%s'",
-        cx_Oracle.DATETIME: "TO_DATE('%s', 'YYYY-MM-DD HH24:MI:SS')"}
-    inputStatementFormatters[cx_Oracle.CLOB] = inputStatementFormatters[cx_Oracle.STRING]
-    inputStatementFormatters[cx_Oracle.TIMESTAMP] = inputStatementFormatters[cx_Oracle.DATETIME]    
-    def output(self, outformat, rowlimit):
-        self.tblname = self.tableNameFinder.search(self.curs.statement).group(1)
-        self.colnames = [d[0] for d in self.curs.description]
-        if outformat in output_templates:
-            self.colnamelen = max(len(colname) for colname in self.colnames)
-            self.coltypes = [d[1] for d in self.curs.description]
-            self.formatters = [self.inputStatementFormatters.get(typ, '%s') for typ in self.coltypes]    
-            result = output_templates[outformat].generate(**self.__dict__)        
-        elif outformat == '\\t': # transposed
-            rows = [self.colnames]
-            rows.extend(list(self.rows))
-            transpr = [[rows[y][x] for y in range(len(rows))]for x in range(len(rows[0]))] # matrix transpose
-            newdesc = [['ROW N.'+str(y),10] for y in range(len(rows))]
-            for x in range(len(self.curs.description)):
-                if str(self.curs.description[x][1]) == "<type 'cx_Oracle.BINARY'>":  # handles RAW columns
-                    rname = transpr[x][0]
-                    transpr[x] = map(binascii.b2a_hex, transpr[x])
-                    transpr[x][0] = rname
-            newdesc[0][0] = 'COLUMN NAME'
-            result = '\n' + sqlpython.pmatrix(transpr,newdesc)            
-        elif outformat in ('\\l', '\\L', '\\p', '\\b'):
-            plot = Plot()
-            plot.build(self, outformat)
-            plot.shelve()                
-            plot.draw()
-            return ''
-        else:
-            result = sqlpython.pmatrix(self.rows, self.curs.description, self.maxfetch)
-        return result
-        
-    legalOracle = re.compile('[a-zA-Z_$#]')
-
-    def select_scalar_list(self, sql, binds={}):
-        self.curs.execute(sql, binds)
-        return [r[0] for r in self.curs.fetchall()]
-    
-    columnNameRegex = re.compile(
-        r'select\s+(.*)from',
-        re.IGNORECASE | re.DOTALL | re.MULTILINE)        
-    def completedefault(self, text, line, begidx, endidx):
-        segment = completion.whichSegment(line)
-        text = text.upper()
-        completions = []
-        if segment == 'select':
-            stmt = "SELECT column_name FROM user_tab_columns WHERE column_name LIKE '%s%%'"
-            completions = self.select_scalar_list(stmt % (text))
-            if not completions:
-                stmt = "SELECT column_name FROM all_tab_columns WHERE column_name LIKE '%s%%'"            
-                completions = self.select_scalar_list(stmt % (text))
-        if segment == 'from':
-            columnNames = self.columnNameRegex.search(line)
-            if columnNames:
-                columnNames = columnNames.group(1)
-                columnNames = [c.strip().upper() for c in columnNames.split(',')]
-                stmt1 = "SELECT table_name FROM all_tab_columns WHERE column_name = '%s' AND table_name LIKE '%s%%'"
-                for columnName in columnNames:
-                    # and if columnName is * ?
-                    completions.extend(self.select_scalar_list(stmt1 % (columnName, text)))                    
-        if segment in ('from', 'update', 'insert into') and (not completions):
-            stmt = "SELECT table_name FROM user_tables WHERE table_name LIKE '%s%%'"
-            completions = self.select_scalar_list(stmt % (text))
-            if not completions:
-                stmt = """SELECT table_name FROM user_tables WHERE table_name LIKE '%s%%'
-                      UNION
-                      SELECT DISTINCT owner FROM all_tables WHERE owner LIKE '%%%s'"""
-                completions = self.select_scalar_list(stmt % (text, text))
-        if segment in ('where', 'group by', 'order by', 'having', 'set'):
-            tableNames = completion.tableNamesFromFromClause(line)
-            if tableNames:
-                stmt = """SELECT column_name FROM all_tab_columns
-                          WHERE table_name IN (%s)""" % \
-                       (','.join("'%s'" % (t) for t in tableNames))
-            stmt = "%s AND column_name LIKE '%s%%'" % (stmt, text)
-            completions = self.select_scalar_list(stmt)
-        if not segment:
-            stmt = "SELECT object_name FROM all_objects WHERE object_name LIKE '%s%%'"
-            completions = self.select_scalar_list(stmt % (text))
-        return completions
-    
-    rowlimitPattern = pyparsing.Word(pyparsing.nums)('rowlimit')
-    rawTerminators = '; \\s \\S \\c \\C \\t \\i \\p \\l \\L \\b ' + ' '.join(output_templates.keys())
-    terminatorPattern = (pyparsing.oneOf(rawTerminators)    
-                        ^ pyparsing.Literal('\n/') ^ \
-                        (pyparsing.Literal('\nEOF') + pyparsing.stringEnd)) \
-                        ('terminator') + \
-                        pyparsing.Optional(rowlimitPattern) #+ \
-                        #pyparsing.FollowedBy(pyparsing.LineEnd())
-    def do_select(self, arg, bindVarsIn=None, override_terminator=None):
-        """Fetch rows from a table.
-
-        Limit the number of rows retrieved by appending
-        an integer after the terminator
-        (example: SELECT * FROM mytable;10 )
-
-        Output may be formatted by choosing an alternative terminator
-        ("help terminators" for details)
-        """
-        bindVarsIn = bindVarsIn or {}
-        statement = self.parsed('select ' + arg)
-        self.query = statement.unterminated
-        if override_terminator:
-            statement['terminator'] = override_terminator
-        statement['rowlimit'] = int(statement.rowlimit or 0)
-        try:
-            self.varsUsed = findBinds(self.query, self.binds, bindVarsIn)
-            self.curs.execute(self.query, self.varsUsed)
-            self.rows = self.curs.fetchmany(min(self.maxfetch, (statement.rowlimit or self.maxfetch)))
-            self.desc = self.curs.description
-            self.rc = self.curs.rowcount
-            if self.rc > 0:
-                self.stdout.write('\n%s\n' % (self.output(statement.terminator, statement.rowlimit)))
-            if self.rc == 0:
-                print '\nNo rows Selected.\n'
-            elif self.rc == 1: 
-                print '\n1 row selected.\n'
-                if self.autobind:
-                    self.binds.update(dict(zip([''.join(l for l in d[0] if l.isalnum()) for d in self.desc], self.rows[0])))
-                    for (i, val) in enumerate(self.rows[0]):
-                        varname = ''.join(letter for letter in self.desc[i][0] if letter.isalnum() or letter == '_')
-                        self.binds[varname] = val
-                        self.binds[str(i+1)] = val
-            elif self.rc < self.maxfetch:
-                print '\n%d rows selected.\n' % self.rc
-            else:
-                print '\nSelected Max Num rows (%d)' % self.rc
-        except Exception, e:
-            print e
-            import traceback
-            traceback.print_exc(file=sys.stdout)
-        self.sqlBuffer.append(self.query)
-
-        
-    @options([make_option('-f', '--full', action='store_true', help='get dependent objects as well')])
-    def do_pull(self, arg, opts):
-        """Displays source code."""
-
-        arg = self.parsed(arg).unterminated.upper()
-        object_type, owner, object_name = self.resolve(arg)
-        if not object_type:
-            return
-        self.stdout.write("%s %s.%s\n" % (object_type, owner, object_name))
-        self.stdout.write(str(self.curs.callfunc('DBMS_METADATA.GET_DDL', cx_Oracle.CLOB,
-                                                 [object_type, object_name, owner])))
-        if opts.full:
-            for dependent_type in ('OBJECT_GRANT', 'CONSTRAINT', 'TRIGGER'):        
-                try:
-                    self.stdout.write(str(self.curs.callfunc('DBMS_METADATA.GET_DEPENDENT_DDL', cx_Oracle.CLOB,
-                                                             [dependent_type, object_name, owner])))
-                except cx_Oracle.DatabaseError:
-                    pass
-
-    @options([make_option('-a','--all',action='store_true', help='Find in all schemas (not just my own)'),
-              make_option('-i', '--insensitive', action='store_true', help='case-insensitive search'),
-              make_option('-c', '--col', action='store_true', help='find column'),
-              make_option('-t', '--table', action='store_true', help='find table')])                    
-    def do_find(self, arg, opts):
-        """Finds argument in source code or (with -c) in column definitions."""
-
-        arg = self.parsed(arg).unterminated.upper()     
-        
-        if opts.col:
-            sql = "owner, table_name, column_name from all_tab_columns where column_name like '%%%s%%'" % (arg)
-        elif opts.table:
-            sql = "owner, table_name from all_tables where table_name like '%%%s%%'" % (arg)
-        else:
-            if opts.insensitive:
-                searchfor = "LOWER(text)"
-                arg = arg.lower()
-            else:
-                searchfor = "text"
-            sql = "* from all_source where %s like '%%%s%%'" % (searchfor, arg)
-        if not opts.all:
-            sql = '%s and owner = user' % (sql)
-        self.do_select(sql)
-
-    @options([make_option('-a','--all',action='store_true',
-                          help='Describe all objects (not just my own)')])
-    def do_describe(self, arg, opts):
-        "emulates SQL*Plus's DESCRIBE"
-
-        arg = self.parsed(arg).unterminated.upper()
-        if opts.all:
-            which_view = (', owner', 'all')
-        else:
-            which_view = ('', 'user')
-
-        if not arg:
-            self.do_select("""object_name, object_type%s FROM %s_objects WHERE object_type IN ('TABLE','VIEW','INDEX') ORDER BY object_name""" % which_view)
-            return
-        object_type, owner, object_name = self.resolve(arg)
-        if not object_type:
-            self.do_select("""object_name, object_type%s FROM %s_objects
-                           WHERE object_type IN ('TABLE','VIEW','INDEX')
-                           AND   object_name LIKE '%%%s%%'
-                           ORDER BY object_name""" %
-                           (which_view[0], which_view[1], arg.upper()) )
-            return                    
-        self.stdout.write("%s %s.%s\n" % (object_type, owner, object_name))
-        descQ = descQueries.get(object_type)
-        if descQ:
-            for q in descQ:
-                self.do_select(q,bindVarsIn={'object_name':object_name, 'owner':owner})
-        elif object_type == 'PACKAGE':
-            packageContents = self.select_scalar_list(descQueries['PackageObjects'][0], {'package_name':object_name, 'owner':owner})
-            for packageObj_name in packageContents:
-                self.stdout.write('Arguments to %s\n' % (packageObj_name))
-                self.do_select(descQueries['PackageObjArgs'][0],bindVarsIn={'package_name':object_name, 'owner':owner, 'object_name':packageObj_name})
-    do_desc = do_describe
-
-    def do_deps(self, arg):
-        arg = self.parsed(arg).unterminated.upper()        
-        object_type, owner, object_name = self.resolve(arg)
-        if object_type == 'PACKAGE BODY':
-            q = "and (type != 'PACKAGE BODY' or name != :object_name)'"
-            object_type = 'PACKAGE'
-        else:
-            q = ""
-        q = """         name,
-          type
-          from user_dependencies
-          where
-          referenced_name like :object_name
-          and	referenced_type like :object_type
-          and	referenced_owner like :owner
-          %s""" % (q)
-        self.do_select(q, {'object_name':object_name, 'object_type':object_type, 'owner':owner})
-
-    def do_comments(self, arg):
-        'Prints comments on a table and its columns.'
-        arg = self.parsed(arg).unterminated.upper()        
-        object_type, owner, object_name, colName = self.resolve_with_column(arg)
-        if object_type:
-            self.curs.execute(queries['tabComments'],{'table_name':object_name, 'owner':owner})
-            self.stdout.write("%s %s.%s: %s\n" % (object_type, owner, object_name, self.curs.fetchone()[0]))
-            if colName:
-                self.do_select(queries['oneColComments'],bindVarsIn={'owner':owner, 'object_name': object_name, 'column_name': colName})
-            else:
-                self.do_select(queries['colComments'],bindVarsIn={'owner':owner, 'object_name': object_name})
-
-    def resolve(self, identifier):
-        """Checks (my objects).name, (my synonyms).name, (public synonyms).name
-        to resolve a database object's name. """
-        parts = identifier.split('.')
-        try:
-            if len(parts) == 2:
-                owner, object_name = parts
-                object_type = self.select_scalar_list('SELECT object_type FROM all_objects WHERE owner = :owner AND object_name = :object_name',
-                                  {'owner': owner, 'object_name': object_name}
-                                  )[0]
-            elif len(parts) == 1:
-                object_name = parts[0]
-                self.curs.execute(queries['resolve'], {'objName':object_name})
-                object_type, object_name, owner = self.curs.fetchone()
-        except (TypeError, IndexError):
-            print 'Could not resolve object %s.' % identifier
-            object_type, owner, object_name = '', '', ''
-        return object_type, owner, object_name
-
-    def resolve_with_column(self, identifier):
-        colName = None
-        object_type, owner, object_name = self.resolve(identifier)
-        if not object_type:
-            parts = identifier.split('.')
-            if len(parts) > 1:
-                colName = parts[-1]
-                identifier = '.'.join(parts[:-1])
-                object_type, owner, object_name = self.resolve(identifier)
-        return object_type, owner, object_name, colName
-        
-    def do_resolve(self, arg):
-        arg = self.parsed(arg).unterminated.upper()        
-        self.stdout.write(','.join(self.resolve(arg))+'\n')
-
-    def spoolstop(self):
-        if self.spoolFile:
-            self.stdout = self.stdoutBeforeSpool
-            print 'Finished spooling to ', self.spoolFile.name
-            self.spoolFile.close()
-            self.spoolFile = None
-
-    def do_spool(self, arg):
-        """spool [filename] - begins redirecting output to FILENAME."""
-        self.spoolstop()
-        arg = arg.strip()
-        if not arg:
-            arg = 'output.lst'
-        if arg.lower() != 'off':
-            if '.' not in arg:
-                arg = '%s.lst' % arg
-            print 'Sending output to %s (until SPOOL OFF received)' % (arg)
-            self.spoolFile = open(arg, 'w')
-            self.stdout = self.spoolFile
-
-    def do_write(self, args):
-        print 'Use (query) > outfilename instead.'
-        return
-
-    def do_compare(self, args):
-        """COMPARE query1 TO query2 - uses external tool to display differences.
-
-        Sorting is recommended to avoid false hits.
-        Will attempt to use a graphical diff/merge tool like kdiff3, meld, or Araxis Merge, 
-        if they are installed."""
-        fnames = []
-        args2 = args.split(' to ')
-        if len(args2) < 2:
-            print self.do_compare.__doc__
-            return
-        for n in range(len(args2)):
-            query = args2[n]
-            fnames.append('compare%s.txt' % n)
-            #TODO: update this terminator-stripping
-            if query.rstrip()[-1] != self.terminator: 
-                query = '%s%s' % (query, self.terminator)
-            self.onecmd_plus_hooks('%s > %s' % (query, fnames[n]))
-        diffMergeSearcher.invoke(fnames[0], fnames[1])
-
-    bufferPosPattern = re.compile('\d+')
-    rangeIndicators = ('-',':')
-
-    def do_psql(self, arg):
-        '''Shortcut commands emulating psql's backslash commands.
-
-        \c connect
-        \d desc
-        \e edit
-        \g run
-        \h help
-        \i load
-        \o spool
-        \p list
-        \q quit
-        \w save
-        \db _dir_tablespaces
-        \dd comments
-        \dn _dir_schemas
-        \dt _dir_tables
-        \dv _dir_views
-        \di _dir_indexes
-        \? help psql'''
-        commands = {}
-        for c in self.do_psql.__doc__.splitlines()[2:]:
-            (abbrev, command) = c.split(None, 1)
-            commands[abbrev[1:]] = command
-        words = arg.split(None,1)
-        try:
-            abbrev = words[0]
-        except IndexError:
-            return
-        try:
-            args = words[1]
-        except IndexError:
-            args = ''
-        try:
-            return self.onecmd('%s %s' % (commands[abbrev], args))
-        except KeyError:
-            print 'psql command \%s not yet supported.' % abbrev
-
-    @options([make_option('-a','--all',action='store_true',
-                          help='Describe all objects (not just my own)')])
-    def do__dir_tables(self, arg, opts):
-        if opts.all:
-            which_view = (', owner', 'all')
-        else:
-            which_view = ('', 'user')        
-        self.do_select("""table_name, 'TABLE' as type%s FROM %s_tables WHERE table_name LIKE '%%%s%%'""" %
-                       (which_view[0], which_view[1], arg.upper()))        
-
-    @options([make_option('-a','--all',action='store_true',
-                          help='Describe all objects (not just my own)')])
-    def do__dir_views(self, arg, opts):
-        if opts.all:
-            which_view = (', owner', 'all')
-        else:
-            which_view = ('', 'user')        
-        self.do_select("""view_name, 'VIEW' as type%s FROM %s_views WHERE view_name LIKE '%%%s%%'""" %
-                       (which_view[0], which_view[1], arg.upper())) 
-
-    @options([make_option('-a','--all',action='store_true',
-                          help='Describe all objects (not just my own)')])
-    def do__dir_indexes(self, arg, opts):
-        if opts.all:
-            which_view = (', owner', 'all')
-        else:
-            which_view = ('', 'user')        
-        self.do_select("""index_name, index_type%s FROM %s_indexes WHERE index_name LIKE '%%%s%%' OR table_name LIKE '%%%s%%'""" %
-                       (which_view[0], which_view[1], arg.upper(), arg.upper())) 
-
-    def do__dir_tablespaces(self, arg):
-        self.do_select("""tablespace_name, file_name from dba_data_files""") 
-
-    def do__dir_schemas(self, arg):
-        self.do_select("""owner, count(*) AS objects FROM all_objects GROUP BY owner ORDER BY owner""") 
-
-    def do_head(self, arg):
-        nrows = 10
-        args = arg.split()
-        if len(args) > 1:
-            for a in args:
-                if a[0] == '-':
-                    try:
-                        nrows = int(a[1:])
-                        args.remove(a)
-                    except:
-                        pass
-            arg = ' '.join(args)
-        self.do_select('* from %s;%d' % (arg, nrows))
-
-    def do_print(self, arg):
-        'print VARNAME: Show current value of bind variable VARNAME.'
-        if arg:
-            if arg[0] == ':':
-                arg = arg[1:]
-            try:
-                self.stdout.write(str(self.binds[arg])+'\n')
-            except KeyError:
-                self.stdout.write('No bind variable %s\n' % arg)
-        else:
-            for (var, val) in self.binds.items():
-                print ':%s = %s' % (var, val)
-
-    assignmentScanner = Parser(pyparsing.Literal(':=') ^ '=')
-    def do_setbind(self, arg):
-        if not arg:
-            return self.do_print(arg)
-        arg = self.parsed(arg).unterminated
-        try:
-            assigner, startat, endat = self.assignmentScanner.scanner.scanString(arg).next()
-        except StopIteration:
-            self.do_print(arg)
-            return
-        var, val = arg[:startat].strip(), arg[endat:].strip()
-        if val[0] == val[-1] == "'" and len(val) > 1:
-            self.binds[var] = val[1:-1]
-            return
-        try:
-            self.binds[var] = int(val)
-            return
-        except ValueError:
-            try:
-                self.binds[var] = float(val)
-                return
-            except ValueError: 
-                statekeeper = Statekeeper(self, ('autobind',))  
-                self.autobind = True
-                self.do_select('%s AS %s FROM dual;' % (val, var))
-                statekeeper.restore()
-
-    def do_exec(self, arg):
-        if arg[0] == ':':
-            self.do_setbind(arg[1:])
-        else:
-            arg = self.parsed(arg).unterminated
-            varsUsed = findBinds(arg, self.binds, {})
-            try:
-                self.curs.execute('begin\n%s;end;' % arg, varsUsed)
-            except Exception, e:
-                print e
-
-    '''
-    Fails:
-    select n into :n from test;'''
-    
-    def anon_plsql(self, line1):
-        lines = [line1]
-        while True:
-            line = self.pseudo_raw_input(self.continuationPrompt)
-            if line.strip() == '/':
-                try:
-                    self.curs.execute('\n'.join(lines))
-                except Exception, e:
-                    print e
-                return
-            lines.append(line)
-
-    def do_begin(self, arg):
-        self.anon_plsql('begin ' + arg)
-
-    def do_declare(self, arg):
-        self.anon_plsql('declare ' + arg)
-
-    #def do_create(self, arg):
-    #    self.anon_plsql('create ' + arg)
-
-    @options([make_option('-l', '--long', action='store_true', help='long descriptions'),
-              make_option('-a', '--all', action='store_true', help="all schemas' objects")])        
-    def do_ls(self, arg, opts):
-        where = ''
-        if arg:
-            where = """\nWHERE object_type || '/' || object_name
-                  LIKE '%%%s%%'""" % (arg.upper().replace('*','%'))
-        else:
-            where = ''
-        if opts.all:
-            whose = 'all'
-            objname = "owner || '.' || object_name"            
-        else:
-            whose = 'user'
-            objname = 'object_name'            
-        if opts.long:
-            extraInfo = ', status, last_ddl_time AS modified'
-        else:
-            extraInfo = ''
-        statement = '''SELECT object_type || '/' || %s AS name %s 
-                  FROM   %s_objects %s
-                  ORDER BY object_type, object_name;''' % (objname, extraInfo, whose, where)
-        self.onecmd(statement)
-        
-    def do_cat(self, arg):
-        '''cat TABLENAME --> SELECT * FROM equivalent'''
-        if not arg:
-            print self.do_cat.__doc__
-            return
-        arg = self.parsed(arg)
-        targets = arg.unterminated.split()
-        for target in targets:
-            self.do_select('* from %s%s%s' % (target, arg.terminator, arg.rowlimit)) # permissive of space before terminator
-
-    @options([make_option('-i', '--ignore-case', dest='ignorecase', action='store_true', help='Case-insensitive search')])        
-    def do_grep(self, arg, opts):
-        """grep PATTERN TABLE - search for term in any of TABLE's fields"""    
-
-        arg = self.parsed(arg)
-        targetnames = arg.unterminated.split()
-        pattern = targetnames.pop(0)
-        targets = [] 
-        for target in targetnames:
-            if '*' in target:
-                self.curs.execute("SELECT owner, table_name FROM all_tables WHERE table_name LIKE '%s'%s" %
-                                  (target.upper().replace('*','%')), arg.terminator)
-                for row in self.curs:
-                    targets.append('%s.%s' % row)
-            else:
-                targets.append(target)
-        for target in targets:
-            print target
-            target = target.rstrip(';')
-            sql = []
-            try:
-                self.curs.execute('select * from %s where 1=0' % target) # just to fill description
-                if opts.ignorecase:
-                    sql = ' or '.join("LOWER(%s) LIKE '%%%s%%'" % (d[0], pattern.lower()) for d in self.curs.description)                                        
-                else:
-                    sql = ' or '.join("%s LIKE '%%%s%%'" % (d[0], pattern) for d in self.curs.description)
-                sql = '* FROM %s WHERE %s' % (target, sql)
-                self.do_select('%s%s%s' % (sql, arg.terminator, arg.rowlimit))
-            except Exception, e:
-                print e
-                import traceback
-                traceback.print_exc(file=sys.stdout)                
-
-    def do_refs(self, arg):
-        arg = self.parsed(arg).unterminated.upper()        
-        object_type, owner, object_name = self.resolve(arg)
-        if object_type == 'TABLE':
-            self.do_select(queries['refs'],bindVarsIn={'object_name':object_name, 'owner':owner})
-   
-def _test():
-    import doctest
-    doctest.testmod()
-    
-if __name__ == "__main__":
-    "Silent return implies that all unit tests succeeded.  Use -v to see details."
+"""sqlpyPlus - extra features (inspired	 by Oracle SQL*Plus) for Luca Canali's sqlpython.py
+
+Features include:
+ - SQL*Plus-style bind variables
+ - `set autobind on` stores single-line result sets in bind variables automatically
+ - SQL buffer with list, run, ed, get, etc.; unlike SQL*Plus, buffer stores session's full history
+ - @script.sql loads and runs (like SQL*Plus)
+ - ! runs operating-system command
+ - show and set to control sqlpython parameters
+ - SQL*Plus-style describe, spool
+ - write sends query result directly to file
+ - comments shows table and column comments
+ - compare ... to ... graphically compares results of two queries
+ - commands are case-insensitive
+ - context-sensitive tab-completion for table names, column names, etc.
+
+Use 'help' within sqlpython for details.
+
+Set bind variables the hard (SQL*Plus) way
+exec :b = 3
+or with a python-like shorthand
+:b = 3
+
+- catherinedevlin.blogspot.com  May 31, 2006
+"""
+import sys, os, re, sqlpython, cx_Oracle, pyparsing, re, completion, datetime, pickle
+from cmd2 import Cmd, make_option, options, Statekeeper, Cmd2TestCase
+from output_templates import output_templates
+from plothandler import Plot
+try:
+    import pylab
+except:
+    pass
+
+descQueries = {
+'TABLE': ("""
+          atc.column_name,
+CASE atc.nullable WHEN 'Y' THEN 'NULL' ELSE 'NOT NULL' END "Null?",
+atc.data_type ||
+CASE atc.data_type WHEN 'DATE' THEN ''
+ELSE '(' ||
+CASE atc.data_type WHEN 'NUMBER' THEN TO_CHAR(atc.data_precision) ||
+CASE atc.data_scale WHEN 0 THEN ''
+ELSE ',' || TO_CHAR(atc.data_scale) END
+ELSE TO_CHAR(atc.data_length) END 
+END ||
+CASE atc.data_type WHEN 'DATE' THEN '' ELSE ')' END
+data_type
+FROM all_tab_columns atc
+WHERE atc.table_name = :object_name
+AND      atc.owner = :owner
+ORDER BY atc.column_id;""",),
+'PROCEDURE': ("""
+              NVL(argument_name, 'Return Value') argument_name,             
+data_type,
+in_out,
+default_value
+FROM all_arguments
+WHERE object_name = :object_name
+AND      owner = :owner
+AND      package_name IS NULL
+ORDER BY sequence;""",),    
+'PackageObjects':("""
+SELECT DISTINCT object_name
+FROM all_arguments
+WHERE package_name = :package_name
+AND      owner = :owner""",),
+'PackageObjArgs':("""
+                  object_name,
+argument_name,             
+data_type,
+in_out,
+default_value
+FROM all_arguments
+WHERE package_name = :package_name
+AND      object_name = :object_name
+AND      owner = :owner
+AND      argument_name IS NOT NULL
+ORDER BY sequence""",),
+'TRIGGER':("""
+           description
+FROM   all_triggers
+WHERE  owner = :owner
+AND    trigger_name = :object_name
+""",
+"""
+table_owner,
+base_object_type,
+table_name,
+column_name,
+when_clause,
+status,
+action_type,
+crossedition
+FROM   all_triggers
+WHERE  owner = :owner
+AND    trigger_name = :object_name
+\\t
+""",
+),
+'INDEX':("""
+index_type,
+table_owner,
+table_name,
+table_type,
+uniqueness,
+compression,
+partitioned,
+temporary,
+generated,
+secondary,
+dropped,
+visibility
+FROM   all_indexes
+WHERE  owner = :owner
+AND    index_name = :object_name
+\\t
+""",)
+}
+descQueries['VIEW'] = descQueries['TABLE']
+descQueries['FUNCTION'] = descQueries['PROCEDURE'] 
+
+queries = {
+'resolve': """
+SELECT object_type, object_name, owner FROM (
+SELECT object_type, object_name, user owner, 1 priority
+FROM   user_objects
+WHERE object_name = :objName
+UNION ALL
+SELECT ao.object_type, ao.object_name, ao.owner, 2 priority
+FROM    all_objects ao
+JOIN      user_synonyms us ON (us.table_owner = ao.owner AND us.table_name = ao.object_name)
+WHERE us.synonym_name = :objName
+AND   ao.object_type != 'SYNONYM'
+UNION ALL
+SELECT ao.object_type, ao.object_name, ao.owner, 3 priority
+FROM    all_objects ao
+JOIN      all_synonyms asyn ON (asyn.table_owner = ao.owner AND asyn.table_name = ao.object_name)
+WHERE asyn.synonym_name = :objName
+AND   ao.object_type != 'SYNONYM'
+AND      asyn.owner = 'PUBLIC'
+UNION ALL 
+SELECT 'DIRECTORY' object_type, dir.directory_name, dir.owner, 6 priority
+FROM   all_directories dir
+WHERE  dir.directory_name = :objName
+UNION ALL 
+SELECT 'DATABASE LINK' object_type, db_link, owner, 7 priority
+FROM   all_db_links dbl
+WHERE  dbl.db_link = :objName
+) ORDER BY priority ASC,
+           length(object_type) ASC,
+           object_type DESC""", # preference: PACKAGE before PACKAGE BODY, TABLE before INDEX
+'tabComments': """
+SELECT comments
+FROM    all_tab_comments
+WHERE owner = :owner
+AND      table_name = :table_name""",
+'colComments': """
+atc.column_name,
+acc.comments             
+FROM all_tab_columns atc
+JOIN all_col_comments acc ON (atc.owner = acc.owner and atc.table_name = acc.table_name and atc.column_name = acc.column_name)
+WHERE atc.table_name = :object_name
+AND      atc.owner = :owner
+ORDER BY atc.column_id;""",
+'oneColComments': """
+atc.column_name,
+acc.comments             
+FROM all_tab_columns atc
+JOIN all_col_comments acc ON (atc.owner = acc.owner and atc.table_name = acc.table_name and atc.column_name = acc.column_name)
+WHERE atc.table_name = :object_name
+AND      atc.owner = :owner
+AND      acc.column_name = :column_name
+ORDER BY atc.column_id;""",
+#thanks to Senora.pm for "refs"
+'refs': """
+NULL               referenced_by, 
+c2.table_name      references, 
+c1.constraint_name constraint
+FROM
+user_constraints c1,
+user_constraints c2
+WHERE
+c1.table_name = :object_name
+and c1.constraint_type ='R'
+and c1.r_constraint_name = c2.constraint_name
+and c1.r_owner = c2.owner
+and c1.owner = :owner
+UNION
+SELECT c1.table_name      referenced_by, 
+NULL               references, 
+c1.constraint_name constraint
+FROM
+user_constraints c1,
+user_constraints c2
+WHERE
+c2.table_name = :object_name
+and c1.constraint_type ='R'
+and c1.r_constraint_name = c2.constraint_name
+and c1.r_owner = c2.owner
+and c1.owner = :owner       
+"""
+}
+
+if float(sys.version[:3]) < 2.3:
+    def enumerate(lst):
+        return zip(range(len(lst)), lst)
+
+class SoftwareSearcher(object):
+    def __init__(self, softwareList, purpose):
+        self.softwareList = softwareList
+        self.purpose = purpose
+        self.software = None
+    def invoke(self, *args):
+        if not self.software:
+            (self.software, self.invokeString) = self.find()            
+        argTuple = tuple([self.software] + list(args))
+        os.system(self.invokeString % argTuple)
+    def find(self):
+        if self.purpose == 'text editor':
+            software = os.environ.get('EDITOR')
+            if software:
+                return (software, '%s %s')
+        for (n, (software, invokeString)) in enumerate(self.softwareList):
+            if os.path.exists(software):
+                if n > (len(self.softwareList) * 0.7):
+                    print """
+
+                          Using %s.  Note that there are better options available for %s,
+                          but %s couldn't find a better one in your PATH.
+                          Feel free to open up %s
+                          and customize it to find your favorite %s program.
+
+                          """ % (software, self.purpose, __file__, __file__, self.purpose)
+                return (software, invokeString)
+            stem = os.path.split(software)[1]
+            for p in os.environ['PATH'].split(os.pathsep):
+                if os.path.exists(os.sep.join([p, stem])):
+                    return (stem, invokeString)
+        raise (OSError, """Could not find any %s programs.  You will need to install one,
+               or customize %s to make it aware of yours.
+Looked for these programs:
+%s""" % (self.purpose, __file__, "\n".join([s[0] for s in self.softwareList])))
+    #v2.4: %s""" % (self.purpose, __file__, "\n".join(s[0] for s in self.softwareList)))
+
+softwareLists = {
+    'diff/merge': [  
+        ('/usr/bin/meld',"%s %s %s"),
+        ('/usr/bin/kdiff3',"%s %s %s"),
+        (r'C:\Program Files\Araxis\Araxis Merge v6.5\Merge.exe','"%s" %s %s'),                
+        (r'C:\Program Files\TortoiseSVN\bin\TortoiseMerge.exe', '"%s" /base:"%s" /mine:"%s"'),
+        ('FileMerge','%s %s %s'),        
+        ('kompare','%s %s %s'),   
+        ('WinMerge','%s %s %s'),         
+        ('xxdiff','%s %s %s'),        
+        ('fldiff','%s %s %s'),
+        ('gtkdiff','%s %s %s'),        
+        ('tkdiff','%s %s %s'),         
+        ('gvimdiff','%s %s %s'),        
+        ('diff',"%s %s %s"),
+        (r'c:\windows\system32\comp.exe',"%s %s %s")],
+        'text editor': [
+            ('gedit', '%s %s'),
+            ('textpad', '%s %s'),
+            ('notepad.exe', '%s %s'),
+            ('pico', '%s %s'),
+            ('emacs', '%s %s'),
+            ('vim', '%s %s'),
+            ('vi', '%s %s'),
+            ('ed', '%s %s'),
+            ('edlin', '%s %s')
+        ]
+}
+
+diffMergeSearcher = SoftwareSearcher(softwareLists['diff/merge'],'diff/merge')
+editSearcher = SoftwareSearcher(softwareLists['text editor'], 'text editor')
+editor = os.environ.get('EDITOR')
+if editor:
+    editSearcher.find = lambda: (editor, "%s %s")
+
+class CaselessDict(dict):
+    """dict with case-insensitive keys.
+
+    Posted to ASPN Python Cookbook by Jeff Donner - http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66315"""
+    def __init__(self, other=None):
+        if other:
+            # Doesn't do keyword args
+            if isinstance(other, dict):
+                for k,v in other.items():
+                    dict.__setitem__(self, k.lower(), v)
+            else:
+                for k,v in other:
+                    dict.__setitem__(self, k.lower(), v)
+    def __getitem__(self, key):
+        return dict.__getitem__(self, key.lower())
+    def __setitem__(self, key, value):
+        dict.__setitem__(self, key.lower(), value)
+    def __contains__(self, key):
+        return dict.__contains__(self, key.lower())
+    def has_key(self, key):
+        return dict.has_key(self, key.lower())
+    def get(self, key, def_val=None):
+        return dict.get(self, key.lower(), def_val)
+    def setdefault(self, key, def_val=None):
+        return dict.setdefault(self, key.lower(), def_val)
+    def update(self, other):
+        for k,v in other.items():
+            dict.__setitem__(self, k.lower(), v)
+    def fromkeys(self, iterable, value=None):
+        d = CaselessDict()
+        for k in iterable:
+            dict.__setitem__(d, k.lower(), value)
+        return d
+    def pop(self, key, def_val=None):
+        return dict.pop(self, key.lower(), def_val)
+
+class Parser(object):
+    comment_def = "--" + pyparsing.ZeroOrMore(pyparsing.CharsNotIn("\n"))    
+    def __init__(self, scanner, retainSeparator=True):
+        self.scanner = scanner
+        self.scanner.ignore(pyparsing.sglQuotedString)
+        self.scanner.ignore(pyparsing.dblQuotedString)
+        self.scanner.ignore(self.comment_def)
+        self.scanner.ignore(pyparsing.cStyleComment)
+        self.retainSeparator = retainSeparator
+    def separate(self, txt):
+        itms = []
+        for (sqlcommand, start, end) in self.scanner.scanString(txt):
+            if sqlcommand:
+                if type(sqlcommand[0]) == pyparsing.ParseResults:
+                    if self.retainSeparator:
+                        itms.append("".join(sqlcommand[0]))
+                    else:
+                        itms.append(sqlcommand[0][0])
+                else:
+                    if sqlcommand[0]:
+                        itms.append(sqlcommand[0])
+        return itms
+
+bindScanner = Parser(pyparsing.Literal(':') + pyparsing.Word( pyparsing.alphanums + "_$#" ))   
+    
+def findBinds(target, existingBinds, givenBindVars = {}):
+    result = givenBindVars
+    for finding, startat, endat in bindScanner.scanner.scanString(target):
+        varname = finding[1]
+        try:
+            result[varname] = existingBinds[varname]
+        except KeyError:
+            if not givenBindVars.has_key(varname):
+                print 'Bind variable %s not defined.' % (varname)                
+    return result
+       
+class sqlpyPlus(sqlpython.sqlpython):
+    defaultExtension = 'sql'
+    sqlpython.sqlpython.shortcuts.update({':': 'setbind', 
+                                          '\\': 'psql', 
+                                          '@': '_load'})
+    multilineCommands = '''select insert update delete tselect
+                      create drop alter _multiline_comment'''.split()
+    sqlpython.sqlpython.noSpecialParse.append('spool')
+    commentGrammars = pyparsing.Or([pyparsing.Literal('--') + pyparsing.restOfLine, pyparsing.cStyleComment])
+    defaultFileName = 'afiedt.buf'
+    def __init__(self):
+        sqlpython.sqlpython.__init__(self)
+        self.binds = CaselessDict()
+        self.sqlBuffer = []
+        self.settable = 'autobind commit_on_exit echo maxfetch maxtselctrows timeout'.split()
+        # settables must be lowercase
+        self.stdoutBeforeSpool = sys.stdout
+        self.spoolFile = None
+        self.autobind = False
+    def default(self, arg):
+        sqlpython.sqlpython.default(self, arg)
+        self.sqlBuffer.append(self.query)            
+
+    # overrides cmd's parseline
+    def parseline(self, line):
+        """Parse the line into a command name and a string containing
+        the arguments.  Returns a tuple containing (command, args, line).
+        'command' and 'args' may be None if the line couldn't be parsed.        
+        Overrides cmd.cmd.parseline to accept variety of shortcuts.."""
+
+        cmd, arg, line = sqlpython.sqlpython.parseline(self, line)
+        if cmd in ('select', 'sleect', 'insert', 'update', 'delete', 'describe',
+                   'desc', 'comments', 'pull', 'refs', 'desc', 'triggers', 'find') \
+           and not hasattr(self, 'curs'):
+            print 'Not connected.'
+            return '', '', ''
+        return cmd, arg, line
+    
+    do__load = Cmd.do_load
+
+    def onecmd_plus_hooks(self, line):                          
+        line = self.precmd(line)
+        stop = self.onecmd(line)
+        stop = self.postcmd(stop, line)
+
+    def do_shortcuts(self,arg):
+        """Lists available first-character shortcuts
+        (i.e. '!dir' is equivalent to 'shell dir')"""
+        for (scchar, scto) in self.shortcuts.items():
+            print '%s: %s' % (scchar, scto)
+
+    def colnames(self):
+        return [d[0] for d in curs.description]
+
+    def sql_format_itm(self, itm, needsquotes):
+        if itm is None:
+            return 'NULL'
+        if needsquotes:
+            return "'%s'" % str(itm)
+        return str(itm)
+    tableNameFinder = re.compile(r'from\s+([\w$#_"]+)', re.IGNORECASE | re.MULTILINE | re.DOTALL)          
+    inputStatementFormatters = {
+        cx_Oracle.STRING: "'%s'",
+        cx_Oracle.DATETIME: "TO_DATE('%s', 'YYYY-MM-DD HH24:MI:SS')"}
+    inputStatementFormatters[cx_Oracle.CLOB] = inputStatementFormatters[cx_Oracle.STRING]
+    inputStatementFormatters[cx_Oracle.TIMESTAMP] = inputStatementFormatters[cx_Oracle.DATETIME]    
+    def output(self, outformat, rowlimit):
+        self.tblname = self.tableNameFinder.search(self.curs.statement).group(1)
+        self.colnames = [d[0] for d in self.curs.description]
+        if outformat in output_templates:
+            self.colnamelen = max(len(colname) for colname in self.colnames)
+            self.coltypes = [d[1] for d in self.curs.description]
+            self.formatters = [self.inputStatementFormatters.get(typ, '%s') for typ in self.coltypes]    
+            result = output_templates[outformat].generate(**self.__dict__)        
+        elif outformat == '\\t': # transposed
+            rows = [self.colnames]
+            rows.extend(list(self.rows))
+            transpr = [[rows[y][x] for y in range(len(rows))]for x in range(len(rows[0]))] # matrix transpose
+            newdesc = [['ROW N.'+str(y),10] for y in range(len(rows))]
+            for x in range(len(self.curs.description)):
+                if str(self.curs.description[x][1]) == "<type 'cx_Oracle.BINARY'>":  # handles RAW columns
+                    rname = transpr[x][0]
+                    transpr[x] = map(binascii.b2a_hex, transpr[x])
+                    transpr[x][0] = rname
+            newdesc[0][0] = 'COLUMN NAME'
+            result = '\n' + sqlpython.pmatrix(transpr,newdesc)            
+        elif outformat in ('\\l', '\\L', '\\p', '\\b'):
+            plot = Plot()
+            plot.build(self, outformat)
+            plot.shelve()                
+            plot.draw()
+            return ''
+        else:
+            result = sqlpython.pmatrix(self.rows, self.curs.description, self.maxfetch)
+        return result
+        
+    legalOracle = re.compile('[a-zA-Z_$#]')
+
+    def select_scalar_list(self, sql, binds={}):
+        self.curs.execute(sql, binds)
+        return [r[0] for r in self.curs.fetchall()]
+    
+    columnNameRegex = re.compile(
+        r'select\s+(.*)from',
+        re.IGNORECASE | re.DOTALL | re.MULTILINE)        
+    def completedefault(self, text, line, begidx, endidx):
+        segment = completion.whichSegment(line)
+        text = text.upper()
+        completions = []
+        if segment == 'select':
+            stmt = "SELECT column_name FROM user_tab_columns WHERE column_name LIKE '%s%%'"
+            completions = self.select_scalar_list(stmt % (text))
+            if not completions:
+                stmt = "SELECT column_name FROM all_tab_columns WHERE column_name LIKE '%s%%'"            
+                completions = self.select_scalar_list(stmt % (text))
+        if segment == 'from':
+            columnNames = self.columnNameRegex.search(line)
+            if columnNames:
+                columnNames = columnNames.group(1)
+                columnNames = [c.strip().upper() for c in columnNames.split(',')]
+                stmt1 = "SELECT table_name FROM all_tab_columns WHERE column_name = '%s' AND table_name LIKE '%s%%'"
+                for columnName in columnNames:
+                    # and if columnName is * ?
+                    completions.extend(self.select_scalar_list(stmt1 % (columnName, text)))                    
+        if segment in ('from', 'update', 'insert into') and (not completions):
+            stmt = "SELECT table_name FROM user_tables WHERE table_name LIKE '%s%%'"
+            completions = self.select_scalar_list(stmt % (text))
+            if not completions:
+                stmt = """SELECT table_name FROM user_tables WHERE table_name LIKE '%s%%'
+                      UNION
+                      SELECT DISTINCT owner FROM all_tables WHERE owner LIKE '%%%s'"""
+                completions = self.select_scalar_list(stmt % (text, text))
+        if segment in ('where', 'group by', 'order by', 'having', 'set'):
+            tableNames = completion.tableNamesFromFromClause(line)
+            if tableNames:
+                stmt = """SELECT column_name FROM all_tab_columns
+                          WHERE table_name IN (%s)""" % \
+                       (','.join("'%s'" % (t) for t in tableNames))
+            stmt = "%s AND column_name LIKE '%s%%'" % (stmt, text)
+            completions = self.select_scalar_list(stmt)
+        if not segment:
+            stmt = "SELECT object_name FROM all_objects WHERE object_name LIKE '%s%%'"
+            completions = self.select_scalar_list(stmt % (text))
+        return completions
+    
+    rowlimitPattern = pyparsing.Word(pyparsing.nums)('rowlimit')
+    rawTerminators = '; \\s \\S \\c \\C \\t \\i \\p \\l \\L \\b ' + ' '.join(output_templates.keys())
+    terminatorPattern = (pyparsing.oneOf(rawTerminators)    
+                        ^ pyparsing.Literal('\n/') ^ \
+                        (pyparsing.Literal('\nEOF') + pyparsing.stringEnd)) \
+                        ('terminator') + \
+                        pyparsing.Optional(rowlimitPattern) #+ \
+                        #pyparsing.FollowedBy(pyparsing.LineEnd())
+    def do_select(self, arg, bindVarsIn=None, override_terminator=None):
+        """Fetch rows from a table.
+
+        Limit the number of rows retrieved by appending
+        an integer after the terminator
+        (example: SELECT * FROM mytable;10 )
+
+        Output may be formatted by choosing an alternative terminator
+        ("help terminators" for details)
+        """
+        bindVarsIn = bindVarsIn or {}
+        statement = self.parsed('select ' + arg)
+        self.query = statement.unterminated
+        if override_terminator:
+            statement['terminator'] = override_terminator
+        statement['rowlimit'] = int(statement.rowlimit or 0)
+        self.varsUsed = findBinds(self.query, self.binds, bindVarsIn)
+        self.curs.execute(self.query, self.varsUsed)
+        self.rows = self.curs.fetchmany(min(self.maxfetch, (statement.rowlimit or self.maxfetch)))
+        self.rc = self.curs.rowcount
+        if self.rc > 0:
+            self.stdout.write('\n%s\n' % (self.output(statement.terminator, statement.rowlimit)))
+        if self.rc == 0:
+            print '\nNo rows Selected.\n'
+        elif self.rc == 1: 
+            print '\n1 row selected.\n'
+            if self.autobind:
+                self.binds.update(dict(zip([''.join(l for l in d[0] if l.isalnum()) for d in self.curs.description], self.rows[0])))
+                for (i, val) in enumerate(self.rows[0]):
+                    varname = ''.join(letter for letter in self.curs.description[i][0] if letter.isalnum() or letter == '_')
+                    self.binds[varname] = val
+                    self.binds[str(i+1)] = val
+        elif self.rc < self.maxfetch:
+            print '\n%d rows selected.\n' % self.rc
+        else:
+            print '\nSelected Max Num rows (%d)' % self.rc
+        self.sqlBuffer.append(self.query)
+
+        
+    @options([make_option('-f', '--full', action='store_true', help='get dependent objects as well')])
+    def do_pull(self, arg, opts):
+        """Displays source code."""
+
+        arg = self.parsed(arg).unterminated.upper()
+        object_type, owner, object_name = self.resolve(arg)
+        if not object_type:
+            return
+        self.stdout.write("%s %s.%s\n" % (object_type, owner, object_name))
+        self.stdout.write(str(self.curs.callfunc('DBMS_METADATA.GET_DDL', cx_Oracle.CLOB,
+                                                 [object_type, object_name, owner])))
+        if opts.full:
+            for dependent_type in ('OBJECT_GRANT', 'CONSTRAINT', 'TRIGGER'):        
+                try:
+                    self.stdout.write(str(self.curs.callfunc('DBMS_METADATA.GET_DEPENDENT_DDL', cx_Oracle.CLOB,
+                                                             [dependent_type, object_name, owner])))
+                except cx_Oracle.DatabaseError:
+                    pass
+
+    @options([make_option('-a','--all',action='store_true', help='Find in all schemas (not just my own)'),
+              make_option('-i', '--insensitive', action='store_true', help='case-insensitive search'),
+              make_option('-c', '--col', action='store_true', help='find column'),
+              make_option('-t', '--table', action='store_true', help='find table')])                    
+    def do_find(self, arg, opts):
+        """Finds argument in source code or (with -c) in column definitions."""
+
+        arg = self.parsed(arg).unterminated.upper()     
+        
+        if opts.col:
+            sql = "owner, table_name, column_name from all_tab_columns where column_name like '%%%s%%'" % (arg)
+        elif opts.table:
+            sql = "owner, table_name from all_tables where table_name like '%%%s%%'" % (arg)
+        else:
+            if opts.insensitive:
+                searchfor = "LOWER(text)"
+                arg = arg.lower()
+            else:
+                searchfor = "text"
+            sql = "* from all_source where %s like '%%%s%%'" % (searchfor, arg)
+        if not opts.all:
+            sql = '%s and owner = user' % (sql)
+        self.do_select(sql)
+
+    @options([make_option('-a','--all',action='store_true',
+                          help='Describe all objects (not just my own)')])
+    def do_describe(self, arg, opts):
+        "emulates SQL*Plus's DESCRIBE"
+
+        arg = self.parsed(arg).unterminated.upper()
+        if opts.all:
+            which_view = (', owner', 'all')
+        else:
+            which_view = ('', 'user')
+
+        if not arg:
+            self.do_select("""object_name, object_type%s FROM %s_objects WHERE object_type IN ('TABLE','VIEW','INDEX') ORDER BY object_name""" % which_view)
+            return
+        object_type, owner, object_name = self.resolve(arg)
+        if not object_type:
+            self.do_select("""object_name, object_type%s FROM %s_objects
+                           WHERE object_type IN ('TABLE','VIEW','INDEX')
+                           AND   object_name LIKE '%%%s%%'
+                           ORDER BY object_name""" %
+                           (which_view[0], which_view[1], arg.upper()) )
+            return                    
+        self.stdout.write("%s %s.%s\n" % (object_type, owner, object_name))
+        descQ = descQueries.get(object_type)
+        if descQ:
+            for q in descQ:
+                self.do_select(q,bindVarsIn={'object_name':object_name, 'owner':owner})
+        elif object_type == 'PACKAGE':
+            packageContents = self.select_scalar_list(descQueries['PackageObjects'][0], {'package_name':object_name, 'owner':owner})
+            for packageObj_name in packageContents:
+                self.stdout.write('Arguments to %s\n' % (packageObj_name))
+                self.do_select(descQueries['PackageObjArgs'][0],bindVarsIn={'package_name':object_name, 'owner':owner, 'object_name':packageObj_name})
+    do_desc = do_describe
+
+    def do_deps(self, arg):
+        arg = self.parsed(arg).unterminated.upper()        
+        object_type, owner, object_name = self.resolve(arg)
+        if object_type == 'PACKAGE BODY':
+            q = "and (type != 'PACKAGE BODY' or name != :object_name)'"
+            object_type = 'PACKAGE'
+        else:
+            q = ""
+        q = """         name,
+          type
+          from user_dependencies
+          where
+          referenced_name like :object_name
+          and	referenced_type like :object_type
+          and	referenced_owner like :owner
+          %s""" % (q)
+        self.do_select(q, {'object_name':object_name, 'object_type':object_type, 'owner':owner})
+
+    def do_comments(self, arg):
+        'Prints comments on a table and its columns.'
+        arg = self.parsed(arg).unterminated.upper()        
+        object_type, owner, object_name, colName = self.resolve_with_column(arg)
+        if object_type:
+            self.curs.execute(queries['tabComments'],{'table_name':object_name, 'owner':owner})
+            self.stdout.write("%s %s.%s: %s\n" % (object_type, owner, object_name, self.curs.fetchone()[0]))
+            if colName:
+                self.do_select(queries['oneColComments'],bindVarsIn={'owner':owner, 'object_name': object_name, 'column_name': colName})
+            else:
+                self.do_select(queries['colComments'],bindVarsIn={'owner':owner, 'object_name': object_name})
+
+    def resolve(self, identifier):
+        """Checks (my objects).name, (my synonyms).name, (public synonyms).name
+        to resolve a database object's name. """
+        parts = identifier.split('.')
+        try:
+            if len(parts) == 2:
+                owner, object_name = parts
+                object_type = self.select_scalar_list('SELECT object_type FROM all_objects WHERE owner = :owner AND object_name = :object_name',
+                                  {'owner': owner, 'object_name': object_name}
+                                  )[0]
+            elif len(parts) == 1:
+                object_name = parts[0]
+                self.curs.execute(queries['resolve'], {'objName':object_name})
+                object_type, object_name, owner = self.curs.fetchone()
+        except (TypeError, IndexError):
+            print 'Could not resolve object %s.' % identifier
+            object_type, owner, object_name = '', '', ''
+        return object_type, owner, object_name
+
+    def resolve_with_column(self, identifier):
+        colName = None
+        object_type, owner, object_name = self.resolve(identifier)
+        if not object_type:
+            parts = identifier.split('.')
+            if len(parts) > 1:
+                colName = parts[-1]
+                identifier = '.'.join(parts[:-1])
+                object_type, owner, object_name = self.resolve(identifier)
+        return object_type, owner, object_name, colName
+        
+    def do_resolve(self, arg):
+        arg = self.parsed(arg).unterminated.upper()        
+        self.stdout.write(','.join(self.resolve(arg))+'\n')
+
+    def spoolstop(self):
+        if self.spoolFile:
+            self.stdout = self.stdoutBeforeSpool
+            print 'Finished spooling to ', self.spoolFile.name
+            self.spoolFile.close()
+            self.spoolFile = None
+
+    def do_spool(self, arg):
+        """spool [filename] - begins redirecting output to FILENAME."""
+        self.spoolstop()
+        arg = arg.strip()
+        if not arg:
+            arg = 'output.lst'
+        if arg.lower() != 'off':
+            if '.' not in arg:
+                arg = '%s.lst' % arg
+            print 'Sending output to %s (until SPOOL OFF received)' % (arg)
+            self.spoolFile = open(arg, 'w')
+            self.stdout = self.spoolFile
+
+    def do_write(self, args):
+        print 'Use (query) > outfilename instead.'
+        return
+
+    def do_compare(self, args):
+        """COMPARE query1 TO query2 - uses external tool to display differences.
+
+        Sorting is recommended to avoid false hits.
+        Will attempt to use a graphical diff/merge tool like kdiff3, meld, or Araxis Merge, 
+        if they are installed."""
+        fnames = []
+        args2 = args.split(' to ')
+        if len(args2) < 2:
+            print self.do_compare.__doc__
+            return
+        for n in range(len(args2)):
+            query = args2[n]
+            fnames.append('compare%s.txt' % n)
+            #TODO: update this terminator-stripping
+            if query.rstrip()[-1] != self.terminator: 
+                query = '%s%s' % (query, self.terminator)
+            self.onecmd_plus_hooks('%s > %s' % (query, fnames[n]))
+        diffMergeSearcher.invoke(fnames[0], fnames[1])
+
+    bufferPosPattern = re.compile('\d+')
+    rangeIndicators = ('-',':')
+
+    def do_psql(self, arg):
+        '''Shortcut commands emulating psql's backslash commands.
+
+        \c connect
+        \d desc
+        \e edit
+        \g run
+        \h help
+        \i load
+        \o spool
+        \p list
+        \q quit
+        \w save
+        \db _dir_tablespaces
+        \dd comments
+        \dn _dir_schemas
+        \dt _dir_tables
+        \dv _dir_views
+        \di _dir_indexes
+        \? help psql'''
+        commands = {}
+        for c in self.do_psql.__doc__.splitlines()[2:]:
+            (abbrev, command) = c.split(None, 1)
+            commands[abbrev[1:]] = command
+        words = arg.split(None,1)
+        try:
+            abbrev = words[0]
+        except IndexError:
+            return
+        try:
+            args = words[1]
+        except IndexError:
+            args = ''
+        try:
+            return self.onecmd('%s %s' % (commands[abbrev], args))
+        except KeyError:
+            print 'psql command \%s not yet supported.' % abbrev
+
+    @options([make_option('-a','--all',action='store_true',
+                          help='Describe all objects (not just my own)')])
+    def do__dir_tables(self, arg, opts):
+        if opts.all:
+            which_view = (', owner', 'all')
+        else:
+            which_view = ('', 'user')        
+        self.do_select("""table_name, 'TABLE' as type%s FROM %s_tables WHERE table_name LIKE '%%%s%%'""" %
+                       (which_view[0], which_view[1], arg.upper()))        
+
+    @options([make_option('-a','--all',action='store_true',
+                          help='Describe all objects (not just my own)')])
+    def do__dir_views(self, arg, opts):
+        if opts.all:
+            which_view = (', owner', 'all')
+        else:
+            which_view = ('', 'user')        
+        self.do_select("""view_name, 'VIEW' as type%s FROM %s_views WHERE view_name LIKE '%%%s%%'""" %
+                       (which_view[0], which_view[1], arg.upper())) 
+
+    @options([make_option('-a','--all',action='store_true',
+                          help='Describe all objects (not just my own)')])
+    def do__dir_indexes(self, arg, opts):
+        if opts.all:
+            which_view = (', owner', 'all')
+        else:
+            which_view = ('', 'user')        
+        self.do_select("""index_name, index_type%s FROM %s_indexes WHERE index_name LIKE '%%%s%%' OR table_name LIKE '%%%s%%'""" %
+                       (which_view[0], which_view[1], arg.upper(), arg.upper())) 
+
+    def do__dir_tablespaces(self, arg):
+        self.do_select("""tablespace_name, file_name from dba_data_files""") 
+
+    def do__dir_schemas(self, arg):
+        self.do_select("""owner, count(*) AS objects FROM all_objects GROUP BY owner ORDER BY owner""") 
+
+    def do_head(self, arg):
+        nrows = 10
+        args = arg.split()
+        if len(args) > 1:
+            for a in args:
+                if a[0] == '-':
+                    try:
+                        nrows = int(a[1:])
+                        args.remove(a)
+                    except:
+                        pass
+            arg = ' '.join(args)
+        self.do_select('* from %s;%d' % (arg, nrows))
+
+    def do_print(self, arg):
+        'print VARNAME: Show current value of bind variable VARNAME.'
+        if arg:
+            if arg[0] == ':':
+                arg = arg[1:]
+            try:
+                self.stdout.write(str(self.binds[arg])+'\n')
+            except KeyError:
+                self.stdout.write('No bind variable %s\n' % arg)
+        else:
+            for (var, val) in self.binds.items():
+                print ':%s = %s' % (var, val)
+
+    assignmentScanner = Parser(pyparsing.Literal(':=') ^ '=')
+    def do_setbind(self, arg):
+        if not arg:
+            return self.do_print(arg)
+        arg = self.parsed(arg).unterminated
+        try:
+            assigner, startat, endat = self.assignmentScanner.scanner.scanString(arg).next()
+        except StopIteration:
+            self.do_print(arg)
+            return
+        var, val = arg[:startat].strip(), arg[endat:].strip()
+        if val[0] == val[-1] == "'" and len(val) > 1:
+            self.binds[var] = val[1:-1]
+            return
+        try:
+            self.binds[var] = int(val)
+            return
+        except ValueError:
+            try:
+                self.binds[var] = float(val)
+                return
+            except ValueError: 
+                statekeeper = Statekeeper(self, ('autobind',))  
+                self.autobind = True
+                self.do_select('%s AS %s FROM dual;' % (val, var))
+                statekeeper.restore()
+
+    def do_exec(self, arg):
+        if arg[0] == ':':
+            self.do_setbind(arg[1:])
+        else:
+            arg = self.parsed(arg).unterminated
+            varsUsed = findBinds(arg, self.binds, {})
+            try:
+                self.curs.execute('begin\n%s;end;' % arg, varsUsed)
+            except Exception, e:
+                print e
+
+    '''
+    Fails:
+    select n into :n from test;'''
+    
+    def anon_plsql(self, line1):
+        lines = [line1]
+        while True:
+            line = self.pseudo_raw_input(self.continuationPrompt)
+            if line.strip() == '/':
+                try:
+                    self.curs.execute('\n'.join(lines))
+                except Exception, e:
+                    print e
+                return
+            lines.append(line)
+
+    def do_begin(self, arg):
+        self.anon_plsql('begin ' + arg)
+
+    def do_declare(self, arg):
+        self.anon_plsql('declare ' + arg)
+
+    #def do_create(self, arg):
+    #    self.anon_plsql('create ' + arg)
+
+    @options([make_option('-l', '--long', action='store_true', help='long descriptions'),
+              make_option('-a', '--all', action='store_true', help="all schemas' objects")])        
+    def do_ls(self, arg, opts):
+        where = ''
+        if arg:
+            where = """\nWHERE object_type || '/' || object_name
+                  LIKE '%%%s%%'""" % (arg.upper().replace('*','%'))
+        else:
+            where = ''
+        if opts.all:
+            whose = 'all'
+            objname = "owner || '.' || object_name"            
+        else:
+            whose = 'user'
+            objname = 'object_name'            
+        if opts.long:
+            extraInfo = ', status, last_ddl_time AS modified'
+        else:
+            extraInfo = ''
+        statement = '''SELECT object_type || '/' || %s AS name %s 
+                  FROM   %s_objects %s
+                  ORDER BY object_type, object_name;''' % (objname, extraInfo, whose, where)
+        self.onecmd(statement)
+        
+    def do_cat(self, arg):
+        '''cat TABLENAME --> SELECT * FROM equivalent'''
+        if not arg:
+            print self.do_cat.__doc__
+            return
+        arg = self.parsed(arg)
+        targets = arg.unterminated.split()
+        for target in targets:
+            self.do_select('* from %s%s%s' % (target, arg.terminator or ';', arg.rowlimit)) # permissive of space before terminator
+
+    @options([make_option('-i', '--ignore-case', dest='ignorecase', action='store_true', help='Case-insensitive search')])        
+    def do_grep(self, arg, opts):
+        """grep PATTERN TABLE - search for term in any of TABLE's fields"""    
+
+        arg = self.parsed(arg)
+        targetnames = arg.unterminated.split()
+        pattern = targetnames.pop(0)
+        targets = [] 
+        for target in targetnames:
+            if '*' in target:
+                self.curs.execute("SELECT owner, table_name FROM all_tables WHERE table_name LIKE '%s'%s" %
+                                  (target.upper().replace('*','%')), arg.terminator)
+                for row in self.curs:
+                    targets.append('%s.%s' % row)
+            else:
+                targets.append(target)
+        for target in targets:
+            print target
+            target = target.rstrip(';')
+            sql = []
+            try:
+                self.curs.execute('select * from %s where 1=0' % target) # just to fill description
+                if opts.ignorecase:
+                    sql = ' or '.join("LOWER(%s) LIKE '%%%s%%'" % (d[0], pattern.lower()) for d in self.curs.description)                                        
+                else:
+                    sql = ' or '.join("%s LIKE '%%%s%%'" % (d[0], pattern) for d in self.curs.description)
+                sql = '* FROM %s WHERE %s' % (target, sql)
+                self.do_select('%s%s%s' % (sql, arg.terminator, arg.rowlimit))
+            except Exception, e:
+                print e
+                import traceback
+                traceback.print_exc(file=sys.stdout)                
+
+    def do_refs(self, arg):
+        arg = self.parsed(arg).unterminated.upper()        
+        object_type, owner, object_name = self.resolve(arg)
+        if object_type == 'TABLE':
+            self.do_select(queries['refs'],bindVarsIn={'object_name':object_name, 'owner':owner})
+   
+def _test():
+    import doctest
+    doctest.testmod()
+    
+if __name__ == "__main__":
+    "Silent return implies that all unit tests succeeded.  Use -v to see details."
     _test()