You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cassandra.apache.org by br...@apache.org on 2012/01/26 04:55:30 UTC

git commit: Add read from/write to support for cqlsh. Patch by Paul Cannon, reviewed by brandonwilliams for CASSANDRA-3479

Updated Branches:
  refs/heads/trunk eec230926 -> d5979b301


Add read from/write to support for cqlsh.
Patch by Paul Cannon, reviewed by brandonwilliams for CASSANDRA-3479


Project: http://git-wip-us.apache.org/repos/asf/cassandra/repo
Commit: http://git-wip-us.apache.org/repos/asf/cassandra/commit/d5979b30
Tree: http://git-wip-us.apache.org/repos/asf/cassandra/tree/d5979b30
Diff: http://git-wip-us.apache.org/repos/asf/cassandra/diff/d5979b30

Branch: refs/heads/trunk
Commit: d5979b30171385f6ac780b68ea6271413f07a469
Parents: eec2309
Author: paul cannon <pa...@datastax.com>
Authored: Wed Jan 25 21:46:48 2012 -0600
Committer: Brandon Williams <br...@apache.org>
Committed: Wed Jan 25 21:46:48 2012 -0600

----------------------------------------------------------------------
 bin/cqlsh                     |  504 +++++++++++++++++++++++++-----------
 pylib/cqlshlib/cqlhandling.py |    7 +-
 pylib/cqlshlib/pylexotron.py  |    4 +-
 3 files changed, 354 insertions(+), 161 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cassandra/blob/d5979b30/bin/cqlsh
----------------------------------------------------------------------
diff --git a/bin/cqlsh b/bin/cqlsh
index fd664ec..5d68369 100755
--- a/bin/cqlsh
+++ b/bin/cqlsh
@@ -29,13 +29,15 @@ echo "No appropriate python interpreter found." >&2
 exit 1
 ":"""
 
+from __future__ import with_statement
+
 description = "CQL Shell for Apache Cassandra"
 version = "2.0.0"
 
 from collections import defaultdict
 from StringIO import StringIO
 from itertools import groupby
-from functools import partial
+from contextlib import contextmanager
 
 import cmd
 import sys
@@ -92,6 +94,8 @@ parser.add_option("-C", "--color", action="store_true",
                   help="Enable color output.")
 parser.add_option("-u", "--username", help="Authenticate as user.")
 parser.add_option("-p", "--password", help="Authenticate using password.")
+parser.add_option("-f", "--file",
+                  help="Execute commands from FILE, then exit")
 parser.add_option('--debug', action='store_true',
                   help='Show additional debugging information')
 
@@ -123,7 +127,8 @@ cqlhandling.commands_end_with_newline.update((
     'desc',
     'show',
     'assume',
-    'eof',
+    'source',
+    'capture',
     'exit',
     'quit'
 ))
@@ -135,6 +140,8 @@ cqlhandling.CqlRuleSet.append_rules(r'''
 <specialCommand> ::= <describeCommand>
                    | <showCommand>
                    | <assumeCommand>
+                   | <sourceCommand>
+                   | <captureCommand>
                    | <helpCommand>
                    | <exitCommand>
                    ;
@@ -158,20 +165,21 @@ cqlhandling.CqlRuleSet.append_rules(r'''
                   | "(" colname=<name> ")" "VALUES" "ARE" colvalues=<storageType>
                   ;
 
-<helpCommand> ::= "HELP" [topic]=( <identifier> | <stringLiteral> )*
-                | "?"
+<sourceCommand> ::= "SOURCE" fname=<stringLiteral>
+                  ;
+
+<captureCommand> ::= "CAPTURE" ( fname=( <stringLiteral> | "OFF" ) )?
+                   ;
+
+<helpCommand> ::= ( "HELP" | "?" ) [topic]=( <identifier> | <stringLiteral> )*
                 ;
 
-<exitCommand> ::= ( eof=@"EOF" | "exit" | "quit" )
+<exitCommand> ::= "exit" | "quit"
                 ;
 
 <qmark> ::= "?" ;
 ''')
 
-@cqlhandling.cql_add_completer('exitCommand', 'eof')
-def hide_eof_from_completion(ctxt, cqlsh):
-    return ()
-
 @cqlhandling.cql_add_completer('helpCommand', 'topic')
 def complete_help(ctxt, cqlsh):
     helpfuncs = [n[5:].upper() for n in cqlsh.get_names() if n.startswith('help_')]
@@ -208,6 +216,28 @@ def complete_assume_col(ctxt, cqlsh):
     cols.append(cfdef.key_alias or 'KEY')
     return map(maybe_cql_escape, cols)
 
+def complete_source_quoted_filename(ctxt, cqlsh):
+    partial = ctxt.get_binding('partial', '')
+    head, tail = os.path.split(partial)
+    exhead = os.path.expanduser(head)
+    try:
+        contents = os.listdir(exhead or '.')
+    except OSError:
+        return ()
+    matches = filter(lambda f: f.startswith(tail), contents)
+    annotated = []
+    for f in matches:
+        match = os.path.join(head, f)
+        if os.path.isdir(os.path.join(exhead, f)):
+            match += '/'
+        annotated.append(match)
+    return annotated
+
+cqlhandling.cql_add_completer('sourceCommand', 'fname') \
+        (complete_source_quoted_filename)
+cqlhandling.cql_add_completer('captureCommand', 'fname') \
+        (complete_source_quoted_filename)
+
 class NoKeyspaceError(Exception):
     pass
 
@@ -322,34 +352,48 @@ class Shell(cmd.Cmd):
     display_time_format = '%Y-%m-%d %H:%M:%S%z'
     display_float_precision = 3
     num_retries = 4
+    show_line_nums = False
     debug = False
+    stop = False
+    shunted_query_out = None
 
     def __init__(self, hostname, port, color=False, username=None,
-                 password=None, encoding=None, completekey='tab'):
+                 password=None, encoding=None, stdin=None, tty=True,
+                 completekey='tab', use_conn=None):
         cmd.Cmd.__init__(self, completekey=completekey)
         self.hostname = hostname
         self.port = port
-        self.conn = cql.connect(hostname, port, user=username, password=password)
+        if use_conn is not None:
+            self.conn = use_conn
+        else:
+            self.conn = cql.connect(hostname, port, user=username, password=password)
         self.cursor = self.conn.cursor()
 
         self.current_keyspace = None
 
         self.color = color
         if encoding is None:
-            encoding = sys.stdout.encoding
+            encoding = sys.stdout.encoding or 'ascii'
         self.encoding = encoding
         self.output_codec = codecs.lookup(encoding)
 
         self.statement = StringIO()
+        self.lineno = 1
         self.in_comment = False
         self.schema_overrides = {}
 
-        if sys.stdin.isatty():
-            self.prompt = Shell.default_prompt
+        self.prompt = ''
+        if stdin is None:
+            stdin = sys.stdin
+        self.tty = tty
+        if tty:
+            self.prompt = self.default_prompt
             self.report_connection()
-            self.printout('Use HELP for help.')
+            print 'Use HELP for help.'
         else:
-            self.prompt = ""
+            self.show_line_nums = True
+        self.stdin = stdin
+        self.query_out = sys.stdout
 
     def myformat_value(self, val, casstype):
         return format_value(val, casstype, self.output_codec.name,
@@ -361,40 +405,41 @@ class Shell(cmd.Cmd):
         self.show_version()
 
     def show_host(self):
-        self.printout("Connected to ", newline=False)
-        self.printout(self.get_cluster_name(), color=BLUE, newline=False)
-        self.printout(" at %s:%d." % (self.hostname, self.port))
+        print "Connected to %s at %s:%d." % \
+               (self.applycolor(self.get_cluster_name(), BLUE),
+                self.hostname,
+                self.port)
 
     def show_version(self):
         vers = self.get_cluster_versions()
         vers['shver'] = version
-        self.printout("[cqlsh %(shver)s | Cassandra %(build)s | CQL spec %(cql)s | Thrift protocol %(thrift)s]" % vers)
+        print "[cqlsh %(shver)s | Cassandra %(build)s | CQL spec %(cql)s | Thrift protocol %(thrift)s]" % vers
 
     def show_assumptions(self):
         all_overrides = self.schema_overrides.items()
         all_overrides.sort()
         if all_overrides:
-            self.printout('')
+            print
         else:
-            self.printout('No overrides.')
+            print 'No overrides.'
             return
         for keyspace, ksoverrides in groupby(all_overrides, key=lambda x:x[0][0]):
             keyspace = maybe_cql_escape(keyspace)
-            self.printout('USE %s;' % keyspace)
-            self.printout('')
+            print 'USE %s;' % keyspace
+            print
             for (ks, cf), override in ksoverrides:
                 cf = maybe_cql_escape(cf)
                 if override.default_name_type:
-                    self.printout('ASSUME %s NAMES ARE %s;'
-                                  % (cf, cql_typename(override.default_name_type)))
+                    print 'ASSUME %s NAMES ARE %s;' \
+                          % (cf, cql_typename(override.default_name_type))
                 if override.default_value_type:
-                    self.printout('ASSUME %s VALUES ARE %s;'
-                                  % (cf, cql_typename(override.default_value_type)))
+                    print 'ASSUME %s VALUES ARE %s;' \
+                          % (cf, cql_typename(override.default_value_type))
                 for colname, vtype in override.value_types.items():
                     colname = maybe_cql_escape(colname)
-                    self.printout('ASSUME %s(%s) VALUES ARE %s;'
-                                  % (cf, colname, cql_typename(vtype)))
-        self.printout('')
+                    print 'ASSUME %s(%s) VALUES ARE %s;' \
+                          % (cf, colname, cql_typename(vtype))
+        print
 
     def get_cluster_versions(self):
         try:
@@ -483,66 +528,124 @@ class Shell(cmd.Cmd):
     # ===== end thrift-dependent parts =====
 
     def reset_statement(self):
+        self.reset_prompt()
+        self.statement.truncate(0)
+
+    def reset_prompt(self):
         if self.current_keyspace is None:
-            self.set_prompt(Shell.default_prompt)
+            self.set_prompt(self.default_prompt)
         else:
-            self.set_prompt(Shell.keyspace_prompt % self.current_keyspace)
-        self.statement.truncate(0)
+            self.set_prompt(self.keyspace_prompt % self.current_keyspace)
 
-    def continue_statement(self):
+    def set_continue_prompt(self):
         if self.current_keyspace is None:
-            self.set_prompt(Shell.continue_prompt)
+            self.set_prompt(self.continue_prompt)
         else:
             spaces = ' ' * len(str(self.current_keyspace))
-            self.set_prompt(Shell.keyspace_continue_prompt % spaces)
+            self.set_prompt(self.keyspace_continue_prompt % spaces)
 
-    def precmd(self, line):
-        self.statement.write(line + '\n')
-        return self.statement.getvalue()
+    @contextmanager
+    def prepare_loop(self):
+        readline = None
+        if self.tty and self.completekey:
+            try:
+                import readline
+            except ImportError:
+                pass
+            else:
+                old_completer = readline.get_completer()
+                readline.set_completer(self.complete)
+                readline.parse_and_bind(self.completekey+": complete")
+        try:
+            yield
+        finally:
+            if readline is not None:
+                readline.set_completer(old_completer)
+
+    def get_input_line(self, prompt=''):
+        if self.tty:
+            line = raw_input(self.prompt) + '\n'
+        else:
+            sys.stdout.write(self.prompt)
+            sys.stdout.flush()
+            line = self.stdin.readline()
+            if not len(line):
+                raise EOFError
+        self.lineno += 1
+        return line
+
+    def cmdloop(self):
+        """
+        Adapted from cmd.Cmd's version, because there is literally no way with
+        cmd.Cmd.cmdloop() to tell the difference between "EOF" showing up in
+        input and an actual EOF.
+        """
+        with self.prepare_loop():
+            while not self.stop:
+                try:
+                    line = self.get_input_line(self.prompt)
+                    self.statement.write(line)
+                    if self.onecmd(self.statement.getvalue()):
+                        self.reset_statement()
+                except EOFError:
+                    self.handle_eof()
+                except cql.Error, cqlerr:
+                    self.printerr(str(cqlerr))
+                except KeyboardInterrupt:
+                    self.reset_statement()
+                    print
+
+    def onecmd(self, statement):
+        """
+        Returns true if the statement is complete and was handled (meaning it
+        can be reset).
+        """
 
-    def onecmd(self, line):
         try:
-            statements, in_batch = cqlhandling.cql_split_statements(line)
+            statements, in_batch = cqlhandling.cql_split_statements(statement)
         except pylexotron.LexingError, e:
-            self.printerr('Invalid syntax at line %d, char %d' % (e.linenum, e.charnum))
-            line = line.split('\n')[e.linenum - 1]
-            self.printerr('  %s' % line)
+            if self.show_line_nums:
+                self.printerr('Invalid syntax at char %d' % (e.charnum,))
+            else:
+                self.printerr('Invalid syntax at line %d, char %d'
+                              % (e.linenum, e.charnum))
+            statement = statement.split('\n')[e.linenum - 1]
+            self.printerr('  %s' % statement)
             self.printerr(' %s^' % (' ' * e.charnum))
-            self.reset_statement()
-            return
+            return True
 
         while statements and not statements[-1]:
             statements = statements[:-1]
         if not statements:
-            self.reset_statement()
-            return
+            return True
         if in_batch or statements[-1][-1][0] != 'endtoken':
-            self.continue_statement()
+            self.set_continue_prompt()
             return
-        try:
-            for st in statements:
-                try:
-                    self.handle_statement(st)
-                except Exception, e:
-                    if self.debug:
-                        import traceback
-                        traceback.print_exc()
-                    else:
-                        self.printerr(e)
-        finally:
-            self.reset_statement()
+        for st in statements:
+            try:
+                self.handle_statement(st)
+            except Exception, e:
+                if self.debug:
+                    import traceback
+                    traceback.print_exc()
+                else:
+                    self.printerr(e)
+        return True
+
+    def handle_eof(self):
+        if self.tty:
+            print
+        statement = self.statement.getvalue()
+        if statement.strip():
+            if not self.onecmd(statement + ';'):
+                self.printerr('Incomplete statement at end of file')
+        self.do_exit()
 
     def handle_statement(self, tokens):
         cmdword = tokens[0][1]
-        if cmdword != 'EOF':
-            # and why yes, it /is/ brain-molestingly stupid that cmd uses
-            # the string "EOF" as a sentinel, so that there's no clear way
-            # to tell the difference between someone typing "EOF" and a
-            # real EOF.
-            cmdword = cmdword.lower()
         if cmdword == '?':
             cmdword = 'help'
-        custom_handler = getattr(self, 'do_' + cmdword, None)
+        custom_handler = getattr(self, 'do_' + cmdword.lower(), None)
         if custom_handler:
             parsed = cqlhandling.cql_whole_parse_tokens(tokens, startsymbol='cqlshCommand')
             if parsed and not parsed.remainder:
@@ -649,10 +752,10 @@ class Shell(cmd.Cmd):
     def print_count_result(self):
         if not self.cursor.result:
             return
-        self.printout('count')
-        self.printout('-----')
-        self.printout(self.cursor.result[0])
-        self.printout("")
+        self.writeresult('count')
+        self.writeresult('-----')
+        self.writeresult(self.cursor.result[0])
+        self.writeresult("")
 
     def print_result(self):
         # first pass: see if we have a static column set
@@ -670,7 +773,7 @@ class Shell(cmd.Cmd):
             self.print_static_result()
         else:
             self.print_dynamic_result()
-        self.printout("")
+        self.writeresult("")
 
     def print_static_result(self):
         colnames, coltypes = zip(*self.cursor.description)[:2]
@@ -684,13 +787,13 @@ class Shell(cmd.Cmd):
 
         # print header
         header = ' | '.join(self.applycolor(name.ljust(w), MAGENTA) for (name, w) in zip(colnames, widths))
-        print ' ' + header.rstrip()
-        print '-%s-' % '-+-'.join('-' * w for w in widths)
+        self.writeresult(' ' + header.rstrip())
+        self.writeresult('-%s-' % '-+-'.join('-' * w for w in widths))
 
         # print row data
         for row in formatted_data:
             line = ' | '.join(col.color_rjust(w) for (col, w) in zip(row, widths))
-            print ' ' + line
+            self.writeresult(' ' + line)
 
     def print_dynamic_result(self):
         for row in self.cursor:
@@ -698,7 +801,7 @@ class Shell(cmd.Cmd):
             colnames = [self.applycolor(name, MAGENTA) for name in colnames]
             colvals = [self.myformat_value(val, casstype) for (val, casstype) in zip(row, coltypes)]
             line = ' | '.join(name + ',' + col.coloredval for (col, name) in zip(colvals, colnames))
-            print ' ' + line
+            self.writeresult(' ' + line)
 
     def emptyline(self):
         pass
@@ -735,37 +838,34 @@ class Shell(cmd.Cmd):
                                         debug=debug_completion, startsymbol='cqlshCommand')
 
     def set_prompt(self, prompt):
-        if sys.stdin.isatty():
+        if self.prompt != '':
             self.prompt = prompt
 
-    def print_recreate_keyspace(self, ksdef):
+    def print_recreate_keyspace(self, ksdef, out):
         stratclass = trim_if_present(ksdef.strategy_class, 'org.apache.cassandra.locator.')
         ksname = maybe_cql_escape(ksdef.name)
-        self.printout("CREATE KEYSPACE %s WITH strategy_class = %s"
-                         % (ksname, cql_escape(stratclass)),
-                      newline=False)
+        out.write("CREATE KEYSPACE %s WITH strategy_class = %s"
+                   % (ksname, cql_escape(stratclass)))
         for opname, opval in ksdef.strategy_options.iteritems():
-            self.printout("\n  AND strategy_options:%s = %s" % (opname, cql_escape(opval)),
-                          newline=False)
-        self.printout(';')
+            out.write("\n  AND strategy_options:%s = %s" % (opname, cql_escape(opval)))
+        out.write(';\n')
 
         if ksdef.cf_defs:
-            self.printout('\nUSE %s;' % ksname)
+            out.write('\nUSE %s;\n' % ksname)
             for cf in ksdef.cf_defs:
-                self.printout('')
-                self.print_recreate_columnfamily(cf)
+                out.write('\n')
+                self.print_recreate_columnfamily(cf, out)
 
-    def print_recreate_columnfamily(self, cfdef):
+    def print_recreate_columnfamily(self, cfdef, out):
         cfname = maybe_cql_escape(cfdef.name)
-        self.printout("CREATE COLUMNFAMILY %s (" % cfname)
+        out.write("CREATE COLUMNFAMILY %s (\n" % cfname)
         alias = cfdef.key_alias if cfdef.key_alias else 'KEY'
         keytype = cql_typename(cfdef.key_validation_class)
-        self.printout("  %s %s PRIMARY KEY" % (alias, keytype), newline=False)
+        out.write("  %s %s PRIMARY KEY" % (alias, keytype))
         indexed_columns = []
         for col in cfdef.column_metadata:
             colname = maybe_cql_escape(col.name)
-            self.printout(",\n  %s %s" % (colname, cql_typename(col.validation_class)),
-                          newline=False)
+            out.write(",\n  %s %s" % (colname, cql_typename(col.validation_class)))
             if col.index_name is not None:
                 indexed_columns.append(col)
         notable_columns = []
@@ -778,65 +878,64 @@ class Shell(cmd.Cmd):
             else:
                 optval = cql_escape(optval)
             notable_columns.append((option, optval))
-        self.printout('\n)', newline=False)
+        out.write('\n)')
         if notable_columns:
             joiner = 'WITH'
             for optname, optval in notable_columns:
-                self.printout(" %s\n  %s=%s" % (joiner, optname, optval), newline=False)
+                out.write(" %s\n  %s=%s" % (joiner, optname, optval))
                 joiner = 'AND'
-        self.printout(";")
+        out.write(";\n")
 
         for col in indexed_columns:
-            self.printout('')
+            out.write('\n')
             # guess CQL can't represent index_type or index_options
-            self.printout('CREATE INDEX %s ON %s (%s);'
-                          % (col.index_name, cfname, maybe_cql_escape(col.name)))
+            out.write('CREATE INDEX %s ON %s (%s);\n'
+                         % (col.index_name, cfname, maybe_cql_escape(col.name)))
 
     def describe_keyspace(self, ksname):
-        self.printout('')
-        self.print_recreate_keyspace(self.get_keyspace(ksname))
-        self.printout('')
+        print
+        self.print_recreate_keyspace(self.get_keyspace(ksname), sys.stdout)
+        print
 
     def describe_columnfamily(self, cfname):
-        self.printout('')
-        self.print_recreate_columnfamily(self.get_columnfamily(cfname))
-        self.printout('')
+        print
+        self.print_recreate_columnfamily(self.get_columnfamily(cfname), sys.stdout)
+        print
 
     def describe_columnfamilies(self, ksname):
         if ksname is None:
             for k in self.get_keyspaces():
-                self.printout('Keyspace %s' % (k.name,))
-                self.printout('---------%s\n' % ('-' * len(k.name)))
+                print 'Keyspace %s' % (k.name,)
+                print '---------%s\n' % ('-' * len(k.name))
                 cmd.Cmd.columnize(self, [c.name for c in k.cf_defs])
-                self.printout('')
+                print
         else:
             try:
                 names = self.get_columnfamily_names(ksname)
             except cql.cassandra.ttypes.NotFoundException:
                 raise KeyspaceNotFound('Keyspace %s not found.' % (ksname,))
-            self.printout('')
+            print
             cmd.Cmd.columnize(self, names)
-            self.printout('')
+            print
 
     def describe_cluster(self):
-        self.printout('Cluster: %s' % self.get_cluster_name())
+        print 'Cluster: %s' % self.get_cluster_name()
         p = trim_if_present(self.get_partitioner(), 'org.apache.cassandra.dht.')
-        self.printout('Partitioner: %s' % p)
+        print 'Partitioner: %s' % p
         snitch = trim_if_present(self.get_snitch(), 'org.apache.cassandra.locator.')
-        self.printout('Snitch: %s' % snitch)
-        self.printout('')
+        print 'Snitch: %s\n' % snitch
         if self.current_keyspace is not None and self.current_keyspace != 'system':
-            self.printout("Range ownership:")
+            print "Range ownership:"
             ring = self.get_ring()
             for entry in ring:
-                self.printout(' %39s  [%s]' % (entry.start_token, ', '.join(entry.endpoints)))
-            self.printout('')
+                print ' %39s  [%s]' % (entry.start_token, ', '.join(entry.endpoints))
+            print
 
     def describe_schema(self):
-        self.printout('')
+        print
         for k in self.get_keyspaces():
-            self.print_recreate_keyspace(k)
-            self.printout('')
+            self.print_recreate_keyspace(k, sys.stdout)
+            print
 
     def do_describe(self, parsed):
         """
@@ -998,33 +1097,116 @@ class Shell(cmd.Cmd):
             self.add_assumption(params['ks'], params['cf'], params['colname'],
                                 overridetype, validator_class)
 
-    def do_EOF(self, parsed):
+    def do_source(self, parsed):
+        """
+        SOURCE [cqlsh only]
+
+        Executes a file containing CQL statements. Gives the output for each
+        statement in turn, if any, or any errors that occur along the way.
+
+        Errors do NOT abort execution of the CQL source file.
+
+        Usage:
+
+          SOURCE '<file>';
+
+        That is, the path to the file to be executed must be given inside a
+        string literal. The path is interpreted relative to the current working
+        directory. The tilde shorthand notation ("~/mydir") is supported for
+        referring to $HOME.
+
+        See also the --file option to cqlsh.
+        """
+
+        fname = parsed.get_binding('fname')
+        fname = os.path.expanduser(cql_dequote(fname))
+        try:
+            f = open(fname, 'r')
+        except IOError, e:
+            self.printerr('Could not open %r: %s' % (fname, e))
+            return
+        subshell = Shell(self.hostname, self.port, color=self.color,
+                         encoding=self.encoding, stdin=f, tty=False,
+                         use_conn=self.conn)
+        subshell.cmdloop()
+        f.close()
+
+    def do_capture(self, parsed):
         """
-        EOF [cqlsh only]
+        CAPTURE [cqlsh only]
+
+        Begins capturing command output and appending it to a specified file.
+        Output will not be shown at the console while it is captured.
 
-        An end-of-file condition on the input stream causes cqlsh to exit.
+        Usage:
 
-        The command 'EOF' also exits cqlsh, but this is only because of an
-        annoying feature of Python's cmd.Cmd, and it is not expected to
-        stay this way. See also 'EXIT', which will continue to work.
+          CAPTURE '<file>';
+          CAPTURE OFF;
+          CAPTURE;
+
+        That is, the path to the file to be executed must be given inside a
+        string literal. The path is interpreted relative to the current working
+        directory. The tilde shorthand notation ("~/mydir") is supported for
+        referring to $HOME.
+
+        Only query result output is captured. Errors and output from cqlsh-only
+        commands will still be shown in the cqlsh session.
+
+        To stop capturing output and show it in the cqlsh session again, use
+        CAPTURE OFF.
+
+        To inspect the current capture configuration, use CAPTURE with no
+        arguments.
         """
 
-        if sys.stdin.isatty(): print
-        self.do_exit(None)
+        fname = parsed.get_binding('fname')
+        if fname is None:
+            if self.shunted_query_out is not None:
+                print "Currently capturing query output to %r." % (self.query_out.name,)
+            else:
+                print "Currently not capturing query output."
+            return
+
+        if fname.upper() == 'OFF':
+            if self.shunted_query_out is None:
+                self.printerr('Not currently capturing output.')
+                return
+            self.query_out.close()
+            self.query_out = self.shunted_query_out
+            self.color = self.shunted_color
+            self.shunted_query_out = None
+            del self.shunted_color
+            return
+
+        if self.shunted_query_out is not None:
+            self.printerr('Already capturing output to %s. Use CAPTURE OFF'
+                          ' to disable.' % (self.query_out.name,))
+            return
 
-    def do_exit(self, parsed):
+        fname = os.path.expanduser(cql_dequote(fname))
+        try:
+            f = open(fname, 'a')
+        except IOError, e:
+            self.printerr('Could not open %r for append: %s' % (fname, e))
+            return
+        self.shunted_query_out = self.query_out
+        self.shunted_color = self.color
+        self.query_out = f
+        self.color = False
+        print 'Now capturing query output to %r.' % (fname,)
+
+    def do_exit(self, parsed=None):
         """
         EXIT/QUIT [cqlsh only]
 
         Exits cqlsh.
         """
-
-        sys.exit()
+        self.stop = True
     do_quit = do_exit
 
     def get_names(self):
         names = cmd.Cmd.get_names(self)
-        for hide_from_help in ('do_EOF', 'do_quit'):
+        for hide_from_help in ('do_quit',):
             names.remove(hide_from_help)
         return names
 
@@ -1046,9 +1228,9 @@ class Shell(cmd.Cmd):
             cmd.Cmd.do_help(self, cql_dequote(t).lower())
 
     def help_types(self):
-        self.printout("\n        CQL types recognized by this version of cqlsh:\n")
+        print "\n        CQL types recognized by this version of cqlsh:\n"
         for t in cqlhandling.cql_types:
-            self.printout('          ' + t)
+            print '          ' + t
         print """
         For information on the various recognizable input formats for these
         types, or on controlling the formatting of cqlsh query output, see
@@ -1670,13 +1852,17 @@ class Shell(cmd.Cmd):
             return text
         return color + text + ANSI_RESET
 
-    def printout(self, text, color=None, newline=True, out=None):
+    def writeresult(self, text, color=None, newline=True, out=None):
         if out is None:
-            out = sys.stdout
+            out = self.query_out
         out.write(self.applycolor(str(text), color) + ('\n' if newline else ''))
 
-    def printerr(self, text, color=RED, newline=True):
-        self.printout(text, color, newline=newline, out=sys.stderr)
+    def printerr(self, text, color=RED, newline=True, shownum=None):
+        if shownum is None:
+            shownum = self.show_line_nums
+        if shownum:
+            text = '%s:%d:%s' % (self.stdin.name, self.lineno, text)
+        self.writeresult(text, color, newline=newline, out=sys.stderr)
 
     def add_assumption(self, ksname, cfname, colname, valtype, valclass):
         try:
@@ -1716,7 +1902,7 @@ def option_with_default(cparser_getter, section, option, default=None):
         return default
 
 def should_use_color():
-    if not sys.stdin.isatty():
+    if not sys.stdout.isatty():
         return False
     if os.environ.get('TERM', 'dumb') == 'dumb':
         return False
@@ -1745,6 +1931,8 @@ def read_options(cmdlineargs, environment):
         # default yes if tty
         optvalues.color = should_use_color()
     optvalues.debug = False
+    optvalues.file = None
+    optvalues.tty = sys.stdin.isatty()
 
     (options, arguments) = parser.parse_args(cmdlineargs, values=optvalues)
 
@@ -1759,6 +1947,10 @@ def read_options(cmdlineargs, environment):
     if len(arguments) > 1:
         port = arguments[1]
 
+    if options.file is not None:
+        options.color = False
+        options.tty = False
+
     try:
         port = int(port)
     except ValueError:
@@ -1774,12 +1966,22 @@ def main(options, hostname, port):
         delims += '.'
         readline.set_completer_delims(delims)
 
+    if options.file is None:
+        stdin = None
+    else:
+        try:
+            stdin = open(options.file, 'r')
+        except IOError, e:
+            sys.exit("Can't open %r: %s" % (options.file, e))
+
     try:
         shell = Shell(hostname,
                       port,
                       color=options.color,
                       username=options.username,
                       password=options.password,
+                      stdin=stdin,
+                      tty=options.tty,
                       completekey=options.completekey)
     except KeyboardInterrupt:
         sys.exit('Connection aborted.')
@@ -1788,24 +1990,10 @@ def main(options, hostname, port):
     if options.debug:
         shell.debug = True
 
-    while True:
-        try:
-            shell.cmdloop()
-        except SystemExit:
-            if readline is not None:
-                readline.write_history_file(HISTORY)
-            break
-        except cql.Error, cqlerr:
-            shell.printerr(str(cqlerr))
-        except KeyboardInterrupt:
-            shell.reset_statement()
-            print
-        except Exception, err:
-            if options.debug:
-                import traceback
-                traceback.print_exc()
-            else:
-                shell.printerr("Exception: %s" % err)
+    shell.cmdloop()
+
+    if readline is not None:
+        readline.write_history_file(HISTORY)
 
 if __name__ == '__main__':
     main(*read_options(sys.argv[1:], os.environ))

http://git-wip-us.apache.org/repos/asf/cassandra/blob/d5979b30/pylib/cqlshlib/cqlhandling.py
----------------------------------------------------------------------
diff --git a/pylib/cqlshlib/cqlhandling.py b/pylib/cqlshlib/cqlhandling.py
index 30c22fb..340d471 100644
--- a/pylib/cqlshlib/cqlhandling.py
+++ b/pylib/cqlshlib/cqlhandling.py
@@ -715,9 +715,10 @@ def cql_complete_single(text, partial, init_bindings={}, ignore_case=True, start
         partial = prefix + partial
     if tokens and tokens[-1][0] == 'unclosedComment':
         return []
+    bindings['partial'] = partial
 
     # find completions for the position
-    completions = CqlRuleSet.complete(startsymbol, tokens, init_bindings)
+    completions = CqlRuleSet.complete(startsymbol, tokens, bindings)
 
     hints, strcompletes = list_bifilter(pylexotron.is_hint, completions)
 
@@ -743,6 +744,10 @@ def cql_complete_single(text, partial, init_bindings={}, ignore_case=True, start
         # fills in the closing quote for us.
         candidates = [cql_escape(cql_dequote(c))[len(prefix)+1:-1] for c in candidates]
 
+        # the above process can result in an empty string; this doesn't help for
+        # completions
+        candidates = filter(None, candidates)
+
     # prefix a space when desirable for pleasant cql formatting
     if tokens:
         newcandidates = []

http://git-wip-us.apache.org/repos/asf/cassandra/blob/d5979b30/pylib/cqlshlib/pylexotron.py
----------------------------------------------------------------------
diff --git a/pylib/cqlshlib/pylexotron.py b/pylib/cqlshlib/pylexotron.py
index fd769a6..ff099ff 100644
--- a/pylib/cqlshlib/pylexotron.py
+++ b/pylib/cqlshlib/pylexotron.py
@@ -89,8 +89,8 @@ class ParseContext:
                               self.remainder, newname)
 
     def __repr__(self):
-        return '<%s matched=%r remainder=%r prodname=%r>' % (self.__class__.__name__, self.matched, self.remainder,
-                                                             self.productionname)
+        return '<%s matched=%r remainder=%r prodname=%r bindings=%r>' \
+               % (self.__class__.__name__, self.matched, self.remainder, self.productionname, self.bindings)
 
 class matcher:
     def __init__(self, arg):