#!/bin/sh
# -*- mode: Python -*-

# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

""":"
# bash code here; finds a suitable python interpreter and execs this file.
# prefer unqualified "python" if suitable:
python -c 'import sys; sys.exit(not (0x020500b0 < sys.hexversion < 0x03000000))' 2>/dev/null \
    && exec python "$0" "$@"
for pyver in 2.6 2.7 2.5; do
    which python$pyver > /dev/null 2>&1 && exec python$pyver "$0" "$@"
done
echo "No appropriate python interpreter found." >&2
exit 1
":"""

from __future__ import with_statement

description = "CQL Shell for Apache Cassandra"
version = "2.2.0"

from StringIO import StringIO
from itertools import groupby
from contextlib import contextmanager
from glob import glob
from functools import partial
from collections import defaultdict

import cmd
import sys
import os
import time
import optparse
import ConfigParser
import codecs
import re
import platform

# cqlsh should run correctly when run out of a Cassandra source tree,
# out of an unpacked Cassandra tarball, and after a proper package install.
cqlshlibdir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', 'pylib')
if os.path.isdir(cqlshlibdir):
    sys.path.insert(0, cqlshlibdir)

from cqlshlib import cqlhandling, pylexotron, wcwidth
from cqlshlib.cqlhandling import (token_dequote, cql_dequote, cql_escape,
                                  maybe_cql_escape, cql_typename)

try:
    import readline
except ImportError:
    readline = None

CQL_LIB_PREFIX = 'cql-internal-only-'
THRIFT_LIB_PREFIX = 'thrift-python-internal-only-'

# use bundled libs for python-cql and thrift, if available. if there
# is a ../lib dir, use bundled libs there preferentially.
ZIPLIB_DIRS = [os.path.join(os.path.dirname(__file__), '..', 'lib')]
myplatform = platform.system()
if myplatform == 'Linux':
    ZIPLIB_DIRS.append('/usr/share/cassandra/lib')

if os.environ.get('CQLSH_NO_BUNDLED', ''):
    ZIPLIB_DIRS = ()

def find_zip(libprefix):
    for ziplibdir in ZIPLIB_DIRS:
        zips = glob(os.path.join(ziplibdir, libprefix + '*.zip'))
        if zips:
            return max(zips)   # probably the highest version, if multiple

cql_zip = find_zip(CQL_LIB_PREFIX)
if cql_zip:
    ver = os.path.splitext(os.path.basename(cql_zip))[0][len(CQL_LIB_PREFIX):]
    sys.path.insert(0, os.path.join(cql_zip, 'cql-' + ver))
thrift_zip = find_zip(THRIFT_LIB_PREFIX)
if thrift_zip:
    sys.path.insert(0, thrift_zip)

try:
    import cql
except ImportError, e:
    sys.exit("\nPython CQL driver not installed, or not on PYTHONPATH.\n"
             'You might try "easy_install cql".\n\n'
             'Python: %s\n'
             'Module load path: %r\n\n'
             'Error: %s\n' % (sys.executable, sys.path, e))

import cql.decoders
from cql.cursor import _COUNT_DESCRIPTION, _VOID_DESCRIPTION


CONFIG_FILE = os.path.expanduser(os.path.join('~', '.cqlshrc'))
HISTORY = os.path.expanduser(os.path.join('~', '.cqlsh_history'))
DEFAULT_HOST = 'localhost'
DEFAULT_PORT = 9160
DEFAULT_CQLVER = '2'

epilog = """Connects to %(DEFAULT_HOST)s:%(DEFAULT_PORT)d by default. These
defaults can be changed by setting $CQLSH_HOST and/or $CQLSH_PORT. When a
host (and optional port number) are given on the command line, they take
precedence over any defaults.""" % globals()

parser = optparse.OptionParser(description=description, epilog=epilog,
                               usage="Usage: %prog [options] [host [port]]",
                               version='cqlsh ' + version)
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')
parser.add_option('--cqlversion', default=DEFAULT_CQLVER,
                  help='Specify a particular CQL version (default: %default).'
                       ' Examples: "2", "3.0.0-beta1"')
parser.add_option("-2", "--cql2", action="store_const", dest='cqlversion', const='2',
                  help="Shortcut notation for --cqlversion=2")
parser.add_option("-3", "--cql3", action="store_const", dest='cqlversion', const='3',
                  help="Shortcut notation for --cqlversion=3")


RED = '\033[0;1;31m'
GREEN = '\033[0;1;32m'
YELLOW = '\033[0;1;33m'
BLUE = '\033[0;1;34m'
MAGENTA = '\033[0;1;35m'
CYAN = '\033[0;1;36m'
WHITE = '\033[0;1;37m'
DARK_MAGENTA = '\033[0;35m'
ANSI_RESET = '\033[0m'

CQL_ERRORS = (cql.Error,)
try:
    from thrift.Thrift import TException
except ImportError:
    pass
else:
    CQL_ERRORS += (TException,)

debug_completion = bool(os.environ.get('CQLSH_DEBUG_COMPLETION', '') == 'YES')

# we want the cql parser to understand our cqlsh-specific commands too
cqlhandling.commands_end_with_newline.update((
    'help',
    '?',
    'describe',
    'desc',
    'show',
    'assume',
    'source',
    'capture',
    'exit',
    'quit'
))

cqlhandling.CqlRuleSet.append_rules(r'''
<cqlshCommand> ::= <CQL_Statement>
                 | <specialCommand> ( ";" | "\n" )
                 ;
<specialCommand> ::= <describeCommand>
                   | <showCommand>
                   | <assumeCommand>
                   | <sourceCommand>
                   | <captureCommand>
                   | <helpCommand>
                   | <exitCommand>
                   ;

<describeCommand> ::= ( "DESCRIBE" | "DESC" ) ( "KEYSPACE" ksname=<name>?
                                  | "COLUMNFAMILY" cfname=<name>
                                  | "COLUMNFAMILIES"
                                  | "SCHEMA"
                                  | "CLUSTER" )
                    ;

<showCommand> ::= "SHOW" what=( "VERSION" | "HOST" | "ASSUMPTIONS" )
                ;

<assumeCommand> ::= "ASSUME" ( ks=<name> "." )? cf=<name> <assumeTypeDef>
                                                          ( "," <assumeTypeDef> )*
                  ;

<assumeTypeDef> ::= "NAMES" "ARE" names=<storageType>
                  | "VALUES" "ARE" values=<storageType>
                  | "(" colname=<name> ")" "VALUES" "ARE" colvalues=<storageType>
                  ;

<sourceCommand> ::= "SOURCE" fname=<stringLiteral>
                  ;

<captureCommand> ::= "CAPTURE" ( fname=( <stringLiteral> | "OFF" ) )?
                   ;

<helpCommand> ::= ( "HELP" | "?" ) [topic]=( <identifier> | <stringLiteral> )*
                ;

<exitCommand> ::= "exit" | "quit"
                ;

<qmark> ::= "?" ;
''')

@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_')]
    funcs_with_docstrings = [n[3:].upper() for n in cqlsh.get_names()
                             if n.startswith('do_') and getattr(cqlsh, n, None).__doc__]
    return sorted(helpfuncs + funcs_with_docstrings)

@cqlhandling.cql_add_completer('describeCommand', 'ksname')
def complete_describe_ks(ctxt, cqlsh):
    return map(maybe_cql_escape, cqlsh.get_keyspace_names())

@cqlhandling.cql_add_completer('describeCommand', 'cfname')
def complete_describe_cf(ctxt, cqlsh):
    return map(maybe_cql_escape, cqlsh.get_columnfamily_names())

@cqlhandling.cql_add_completer('assumeCommand', 'ks')
def complete_assume_ks(ctxt, cqlsh):
    return [maybe_cql_escape(ks) + '.' for ks in cqlsh.get_keyspace_names()]

@cqlhandling.cql_add_completer('assumeCommand', 'cf')
def complete_assume_cf(ctxt, cqlsh):
    ks = ctxt.get_binding('ks', None)
    if ks is not None:
        ks = cql_dequote(ks)
    return map(maybe_cql_escape, cqlsh.get_columnfamily_names(ks))

@cqlhandling.cql_add_completer('assumeTypeDef', 'colname')
def complete_assume_col(ctxt, cqlsh):
    ks = ctxt.get_binding('ks', None)
    ks = cql_dequote(ks) if ks is not None else None
    cf = cql_dequote(ctxt.get_binding('cf'))
    cfdef = cqlsh.get_columnfamily(cf, ksname=ks)
    cols = [cm.name for cm in cfdef.column_metadata]
    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

class KeyspaceNotFound(Exception):
    pass

class VersionNotSupported(Exception):
    pass

class DecodeError(Exception):
    def __init__(self, thebytes, err, expectedtype, colname=None):
        self.thebytes = thebytes
        self.err = err
        self.expectedtype = expectedtype
        self.colname = colname

    def __str__(self):
        return str(self.thebytes)

    def message(self):
        what = 'column name %r' % (self.thebytes,)
        if self.colname is not None:
            what = 'value %r (for column %r)' % (self.thebytes, self.colname)
        return 'Failed to decode %s as %s: %s' % (what, self.expectedtype, self.err)

    def __repr__(self):
        return '<%s %s>' % (self.__class__.__name__, self.message())

def trim_if_present(s, prefix):
    if s.startswith(prefix):
        return s[len(prefix):]
    return s

class FormattedValue:
    def __init__(self, strval, coloredval, displaywidth):
        self.strval = strval
        self.coloredval = coloredval
        self.displaywidth = displaywidth

    def __len__(self):
        return len(self.strval)

    def _pad(self, width, fill=' '):
        if width > self.displaywidth:
            return fill * (width - self.displaywidth)
        else:
            return ''

    def ljust(self, width, fill=' '):
        """
        Similar to self.strval.ljust(width), but takes expected terminal
        display width into account for special characters, and does not
        take color escape codes into account.
        """
        return self.strval + self._pad(width, fill)

    def rjust(self, width, fill=' '):
        """
        Similar to self.strval.rjust(width), but takes expected terminal
        display width into account for special characters, and does not
        take color escape codes into account.
        """
        return self._pad(width, fill) + self.strval

    def color_rjust(self, width, fill=' '):
        """
        Similar to self.rjust(width), but uses this value's colored
        representation, and does not take color escape codes into account
        in determining width.
        """
        return self._pad(width, fill) + self.coloredval

    def color_ljust(self, width, fill=' '):
        """
        Similar to self.ljust(width), but uses this value's colored
        representation, and does not take color escape codes into account
        in determining width.
        """
        return self.coloredval + self._pad(width, fill)

unicode_controlchars_re = re.compile(r'[\x00-\x31\x7f-\xa0]')
controlchars_re = re.compile(r'[\x00-\x31\x7f-\xff]')

def _show_control_chars(match):
    txt = repr(match.group(0))
    if txt.startswith('u'):
        txt = txt[2:-1]
    else:
        txt = txt[1:-1]
    return txt

bits_to_turn_red_re = re.compile(r'\\([^uUx]|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2}|U[0-9a-fA-F]{8})')

def _make_turn_bits_red_f(color1, color2):
    def _turn_bits_red(match):
        txt = match.group(0)
        if txt == '\\\\':
            return '\\'
        return color1 + txt + color2
    return _turn_bits_red

DEFAULT_VALUE_COLORS = dict(
    default=YELLOW,
    text=YELLOW,
    error=RED,
    hex=DARK_MAGENTA,
    timestamp=GREEN,
    int=GREEN,
    float=GREEN,
    decimal=GREEN,
    boolean=GREEN,
    uuid=GREEN,
)

COLUMN_NAME_COLORS = defaultdict(lambda: MAGENTA,
    error=RED,
    hex=DARK_MAGENTA,
)

def unix_time_from_uuid1(u):
    return (u.get_time() - 0x01B21DD213814000) / 10000000.0

def format_value(val, casstype, output_encoding, addcolor=False, time_format='',
                 float_precision=3, colormap=DEFAULT_VALUE_COLORS):
    color = colormap['default']
    coloredval = None
    displaywidth = None

    if val is None:
        bval = 'null'
        color = colormap['error']
    elif isinstance(val, DecodeError):
        casstype = 'BytesType'
        bval = repr(val.thebytes)
        color = colormap['hex']
    elif casstype == 'UTF8Type':
        escapedval = val.replace(u'\\', u'\\\\')
        escapedval = unicode_controlchars_re.sub(_show_control_chars, escapedval)
        bval = escapedval.encode(output_encoding, 'backslashreplace')
        displaywidth = wcwidth.wcswidth(bval.decode(output_encoding))
        if addcolor:
            tbr = _make_turn_bits_red_f(colormap['hex'], colormap['text'])
            coloredval = colormap['text'] + bits_to_turn_red_re.sub(tbr, bval) + ANSI_RESET
    elif casstype in ('DateType', 'TimeUUIDType'):
        if casstype == 'TimeUUIDType':
            val = unix_time_from_uuid1(val)
        timestamp = time.localtime(val)
        bval = time.strftime(time_format, timestamp)
        color = colormap['timestamp']
    elif casstype in ('LongType', 'Int32Type', 'IntegerType', 'CounterColumnType'):
        # base-10 only for now; support others?
        bval = str(val)
        color = colormap['int']
    elif casstype in ('FloatType', 'DoubleType'):
        bval = '%.*g' % (float_precision, val)
        color = colormap['float']
    elif casstype in ('DecimalType', 'UUIDType', 'BooleanType'):
        # let python do these for us
        bval = str(val)
        color = colormap[cql_typename(casstype)]
    elif casstype == 'BytesType':
        bval = ''.join('%02x' % ord(c) for c in val)
        color = colormap['hex']
    else:
        # AsciiType is the only other one known right now, but handle others
        escapedval = val.replace('\\', '\\\\')
        bval = controlchars_re.sub(_show_control_chars, escapedval)
        if addcolor:
            tbr = _make_turn_bits_red_f(colormap['hex'], colormap['text'])
            coloredval = colormap['text'] + bits_to_turn_red_re.sub(tbr, bval) + ANSI_RESET

    if displaywidth is None:
        displaywidth = len(bval)
    if not addcolor:
        coloredval = bval
    elif coloredval is None:
        coloredval = color + bval + ANSI_RESET

    return FormattedValue(bval, coloredval, displaywidth)

class Shell(cmd.Cmd):
    default_prompt  = "cqlsh> "
    continue_prompt = "   ... "
    keyspace_prompt          = "cqlsh:%s> "
    keyspace_continue_prompt = "%s    ... "
    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, stdin=None, tty=True,
                 completekey='tab', use_conn=None, cqlver=None):
        cmd.Cmd.__init__(self, completekey=completekey)
        self.hostname = hostname
        self.port = port
        self.username = username
        self.password = password
        if use_conn is not None:
            self.conn = use_conn
        else:
            self.conn = cql.connect(hostname, port, user=username, password=password)
            self.set_expanded_cql_version(cqlver)
        self.cursor = self.conn.cursor()

        self.current_keyspace = None

        self.color = color
        if encoding is None:
            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 = {}

        self.prompt = ''
        if stdin is None:
            stdin = sys.stdin
        self.tty = tty
        if tty:
            self.prompt = self.default_prompt
            self.report_connection()
            print 'Use HELP for help.'
        else:
            self.show_line_nums = True
        self.stdin = stdin
        self.query_out = sys.stdout

    def set_expanded_cql_version(self, ver):
        while ver.count('.') < 2:
            ver += '.0'
        self.set_cql_version(ver)
        self.cql_version = ver
        ver_parts = ver.split('-', 1) + ['']
        self.cql_ver_tuple = tuple(map(int, ver_parts[0].split('.')) + [ver_parts[1]])

    def cqlver_atleast(self, major, minor=0, patch=0):
        return self.cql_ver_tuple[:3] >= (major, minor, patch)

    def myformat_value(self, val, casstype, **kwargs):
        if isinstance(val, DecodeError):
            self.decoding_errors.append(val)
        return format_value(val, casstype, self.output_codec.name,
                            addcolor=self.color, time_format=self.display_time_format,
                            float_precision=self.display_float_precision, **kwargs)

    def myformat_colname(self, name, nametype):
        return self.myformat_value(name, nametype, colormap=COLUMN_NAME_COLORS)

    def report_connection(self):
        self.show_host()
        self.show_version()

    def show_host(self):
        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
        # system.Versions['cql'] apparently does not reflect changes with
        # set_cql_version.
        vers['cql'] = self.cql_version
        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:
            print
        else:
            print 'No overrides.'
            return
        for keyspace, ksoverrides in groupby(all_overrides, key=lambda x:x[0][0]):
            keyspace = maybe_cql_escape(keyspace)
            print 'USE %s;' % keyspace
            print
            for (ks, cf), override in ksoverrides:
                cf = maybe_cql_escape(cf)
                if override.default_name_type:
                    print 'ASSUME %s NAMES ARE %s;' \
                          % (cf, cql_typename(override.default_name_type))
                if 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)
                    print 'ASSUME %s(%s) VALUES ARE %s;' \
                          % (cf, colname, cql_typename(vtype))
        print

    def get_cluster_versions(self):
        if self.cqlver_atleast(3):
            query = 'select component, version from system."Versions"'
        else:
            query = 'select component, version from system.Versions'
        try:
            self.cursor.execute(query)
            vers = dict(self.cursor)
        except cql.ProgrammingError:
            # older Cassandra; doesn't have system.Versions
            thrift_ver = self.get_thrift_version()
            return {'build': 'unknown', 'cql': 'unknown', 'thrift': thrift_ver}
        return vers

    def get_keyspace_names(self):
        return [k.name for k in self.get_keyspaces()]

    def get_columnfamilies(self, ksname=None):
        if ksname is None:
            ksname = self.current_keyspace
            if ksname is None:
                raise NoKeyspaceError("Not in any keyspace.")
        return self.get_keyspace(ksname).cf_defs

    def get_columnfamily(self, cfname, ksname=None):
        if ksname is None:
            ksname = self.current_keyspace
        cf_defs = self.get_columnfamilies(ksname)
        for c in cf_defs:
            if c.name == cfname:
                return c
        raise KeyError("Unconfigured column family %r" % (cfname,))

    def get_columnfamily_names(self, ksname=None):
        return [c.name for c in self.get_columnfamilies(ksname)]

    def get_index_names(self, ksname=None):
        indnames = []
        for c in self.get_columnfamilies(ksname):
            for md in c.column_metadata:
                if md.index_name is not None:
                    indnames.append(md.index_name)
        return indnames

    def filterable_column_names(self, cfname, ksname=None):
        cfdef = self.get_columnfamily(cfname, ksname=ksname)
        filterable = set()
        if cfdef.key_alias is not None and cfdef.key_alias != 'KEY':
            filterable.add(cfdef.key_alias)
        else:
            filterable.add('KEY')
        for cm in cfdef.column_metadata:
            if cm.index_name is not None:
                filterable.add(cm.name)
        return filterable

    # ===== thrift-dependent parts =====

    def get_cluster_name(self):
        return self.make_hacktastic_thrift_call('describe_cluster_name')

    def get_partitioner(self):
        return self.make_hacktastic_thrift_call('describe_partitioner')

    def get_snitch(self):
        return self.make_hacktastic_thrift_call('describe_snitch')

    def get_thrift_version(self):
        return self.make_hacktastic_thrift_call('describe_version')

    def get_ring(self):
        if self.current_keyspace is None:
            raise NoKeyspaceError("Ring view requires a current non-system keyspace")
        return self.make_hacktastic_thrift_call('describe_ring', self.current_keyspace)

    def get_keyspace(self, ksname):
        try:
            return self.make_hacktastic_thrift_call('describe_keyspace', ksname)
        except cql.cassandra.ttypes.NotFoundException, e:
            raise KeyspaceNotFound('Keyspace %s not found.' % e)

    def get_keyspaces(self):
        return self.make_hacktastic_thrift_call('describe_keyspaces')

    def get_schema_versions(self):
        return self.make_hacktastic_thrift_call('describe_schema_versions')

    def set_cql_version(self, ver):
        try:
            return self.make_hacktastic_thrift_call('set_cql_version', ver)
        except cql.cassandra.ttypes.InvalidRequestException, e:
            raise VersionNotSupported(e.why)

    def make_hacktastic_thrift_call(self, call, *args):
        client = self.conn.client
        return getattr(client, call)(*args)

    # ===== 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(self.default_prompt)
        else:
            self.set_prompt(self.keyspace_prompt % self.current_keyspace)

    def set_continue_prompt(self):
        if self.current_keyspace is None:
            self.set_prompt(self.continue_prompt)
        else:
            spaces = ' ' * len(str(self.current_keyspace))
            self.set_prompt(self.keyspace_continue_prompt % spaces)

    @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, statementtext):
        """
        Returns true if the statement is complete and was handled (meaning it
        can be reset).
        """

        try:
            statements, in_batch = cqlhandling.cql_split_statements(statementtext)
        except pylexotron.LexingError, e:
            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))
            statementline = statementtext.split('\n')[e.linenum - 1]
            self.printerr('  %s' % statementline)
            self.printerr(' %s^' % (' ' * e.charnum))
            return True

        while statements and not statements[-1]:
            statements = statements[:-1]
        if not statements:
            return True
        if in_batch or statements[-1][-1][0] != 'endtoken':
            self.set_continue_prompt()
            return
        for st in statements:
            try:
                self.handle_statement(st, statementtext)
            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, srcstr):
        cmdword = tokens[0][1]
        if cmdword == '?':
            cmdword = 'help'
        custom_handler = getattr(self, 'do_' + cmdword.lower(), None)
        if custom_handler:
            parsed = cqlhandling.cql_whole_parse_tokens(tokens, srcstr=srcstr,
                                                        startsymbol='cqlshCommand')
            if parsed and not parsed.remainder:
                # successful complete parse
                return custom_handler(parsed)
            else:
                return self.handle_parse_error(cmdword, tokens, parsed, srcstr)
        return self.perform_statement(cqlhandling.cql_extract_orig(tokens, srcstr))

    def handle_parse_error(self, cmdword, tokens, parsed, srcstr):
        if cmdword.lower() == 'select':
            # hey, maybe they know about some new syntax we don't. type
            # assumptions won't work, but maybe the query will.
            return self.perform_statement(cqlhandling.cql_extract_orig(tokens, srcstr))
        if parsed:
            self.printerr('Improper %s command (problem at %r).' % (cmdword, parsed.remainder[0]))
        else:
            self.printerr('Improper %s command.' % cmdword)

    def do_use(self, parsed):
        """
        USE <keyspacename>;

        Tells cqlsh and the connected Cassandra instance that you will be
        working in the given keyspace. All subsequent operations on column
        families or indexes will be in the context of this keyspace, unless
        otherwise specified, until another USE command is issued or the
        connection terminates.

        As always, when a keyspace name does not work as a normal identifier or
        number, it can be enclosed in quotes and expressed as a string literal.
        """
        ksname = parsed.get_binding('ksname')
        if self.perform_statement(parsed.extract_orig()):
            self.current_keyspace = cql_dequote(ksname)

    def do_select(self, parsed):
        """
        SELECT [FIRST n] [REVERSED] <selectExpr>
          FROM [<keyspace>.]<columnFamily>
            [USING CONSISTENCY <consistencylevel>]
            [WHERE <clause>]
            [LIMIT m];

        SELECT is used to read one or more records from a Cassandra column
        family. It returns a result-set of rows, where each row consists of a
        key and a collection of columns corresponding to the query.

        For more information, see one of the following:

          HELP SELECT_EXPR
          HELP SELECT_COLUMNFAMILY
          HELP SELECT_WHERE
          HELP SELECT_LIMIT
          HELP CONSISTENCYLEVEL
        """
        ksname = parsed.get_binding('selectks')
        if ksname is not None:
            ksname = cql_dequote(ksname)
        cfname = cql_dequote(parsed.get_binding('selectsource'))
        decoder = self.determine_decoder_for(cfname, ksname=ksname)
        self.perform_statement(parsed.extract_orig(), decoder=decoder)

    def perform_statement(self, statement, decoder=None):
        if not statement:
            return False
        trynum = 1
        while True:
            try:
                self.cursor.execute(statement, decoder=decoder)
                break
            except cql.IntegrityError, err:
                self.printerr("Attempt #%d: %s" % (trynum, str(err)))
                trynum += 1
                if trynum > self.num_retries:
                    return False
                time.sleep(1*trynum)
            except CQL_ERRORS, err:
                self.printerr(str(err))
                return False
            except Exception, err:
                import traceback
                self.printerr(traceback.format_exc())
                return False

        if self.cursor.description is _COUNT_DESCRIPTION:
            self.print_count_result(self.cursor)
        elif self.cursor.description is not _VOID_DESCRIPTION:
            self.print_result(self.cursor)
        return True

    def determine_decoder_for(self, cfname, ksname=None):
        decoder = ErrorHandlingSchemaDecoder
        if ksname is None:
            ksname = self.current_keyspace
        overrides = self.schema_overrides.get((ksname, cfname), None)
        if overrides:
            decoder = partial(decoder, overrides=overrides)
        return decoder

    def get_nametype(self, cursor, num):
        """
        Determine the Cassandra type of a column name from the current row of
        query results on the given cursor. The column in question is given by
        its zero-based ordinal number within the row.

        This is necessary to differentiate some things like ascii vs. blob hex.
        """

        if getattr(cursor, 'supports_name_info', False):
            return cursor.name_info[num][1]

        # This is a pretty big hack, but necessary until we can rely on
        # python-cql 1.0.10 being around.
        row = cursor.result[cursor.rs_idx - 1]
        col = row.columns[num]
        schema = cursor.decoder.schema
        return schema.name_types.get(col.name, schema.default_name_type)

    def print_count_result(self, cursor):
        if not cursor.result:
            return
        self.writeresult('count')
        self.writeresult('-----')
        self.writeresult(cursor.result[0])
        self.writeresult("")

    def print_result(self, cursor):
        self.decoding_errors = []

        # first pass: see if we have a static column set
        last_description = None
        for row in cursor:
            if last_description is not None and cursor.description != last_description:
                static = False
                break
            last_description = cursor.description
        else:
            static = True
        cursor._reset()

        if static:
            self.print_static_result(cursor)
        else:
            self.print_dynamic_result(cursor)
        self.writeresult("")

        if self.decoding_errors:
            for err in self.decoding_errors[:2]:
                self.writeresult(err.message(), color=RED)
            if len(self.decoding_errors) > 2:
                self.writeresult('%d more decoding errors suppressed.'
                                 % (len(self.decoding_errors) - 2), color=RED)

    def print_static_result(self, cursor):
        colnames, coltypes = zip(*cursor.description)[:2]
        colnames_t = [(name, self.get_nametype(cursor, n)) for (n, name) in enumerate(colnames)]
        formatted_names = [self.myformat_colname(name, nametype) for (name, nametype) in colnames_t]
        formatted_data = [map(self.myformat_value, row, coltypes) for row in cursor]

        # determine column widths
        widths = [n.displaywidth for n in formatted_names]
        for fmtrow in formatted_data:
            for num, col in enumerate(fmtrow):
                widths[num] = max(widths[num], col.displaywidth)

        # print header
        header = ' | '.join(hdr.color_ljust(w) for (hdr, w) in zip(formatted_names, 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))
            self.writeresult(' ' + line)

    def print_dynamic_result(self, cursor):
        for row in cursor:
            colnames, coltypes = zip(*cursor.description)[:2]
            colnames_t = [(name, self.get_nametype(cursor, n)) for (n, name) in enumerate(colnames)]
            colnames = [self.myformat_colname(name, nametype) for (name, nametype) in colnames_t]
            colvals = [self.myformat_value(val, casstype) for (val, casstype) in zip(row, coltypes)]
            line = ' | '.join('%s,%s' % (n.coloredval, v.coloredval) for (n, v) in zip(colnames, colvals))
            self.writeresult(' ' + line)

    def emptyline(self):
        pass

    def parseline(self, line):
        # this shouldn't be needed
        raise NotImplementedError

    def complete(self, text, state):
        if readline is None:
            return
        if state == 0:
            try:
                self.completion_matches = self.find_completions(text)
            except Exception:
                if debug_completion:
                    import traceback
                    traceback.print_exc()
                else:
                    raise
        try:
            return self.completion_matches[state]
        except IndexError:
            return None

    def find_completions(self, text):
        curline = readline.get_line_buffer()
        prevlines = self.statement.getvalue()
        wholestmt = prevlines + curline
        begidx = readline.get_begidx() + len(prevlines)
        endidx = readline.get_endidx() + len(prevlines)
        stuff_to_complete = wholestmt[:begidx]
        return cqlhandling.cql_complete(stuff_to_complete, text, cassandra_conn=self,
                                        debug=debug_completion, startsymbol='cqlshCommand')

    def set_prompt(self, prompt):
        if self.prompt != '':
            self.prompt = prompt

    def print_recreate_keyspace(self, ksdef, out):
        stratclass = trim_if_present(ksdef.strategy_class, 'org.apache.cassandra.locator.')
        ksname = maybe_cql_escape(ksdef.name)
        out.write("CREATE KEYSPACE %s WITH strategy_class = %s"
                   % (ksname, cql_escape(stratclass)))
        for opname, opval in ksdef.strategy_options.iteritems():
            out.write("\n  AND strategy_options:%s = %s" % (opname, cql_escape(opval)))
        out.write(';\n')

        if ksdef.cf_defs:
            out.write('\nUSE %s;\n' % ksname)
            for cf in ksdef.cf_defs:
                out.write('\n')
                self.print_recreate_columnfamily(cf, out)

    def print_recreate_columnfamily(self, cfdef, out):
        cfname = maybe_cql_escape(cfdef.name)
        out.write("CREATE COLUMNFAMILY %s (\n" % cfname)
        alias = cfdef.key_alias if cfdef.key_alias else 'KEY'
        keytype = cql_typename(cfdef.key_validation_class)
        out.write("  %s %s PRIMARY KEY" % (alias, keytype))
        indexed_columns = []
        for col in cfdef.column_metadata:
            colname = maybe_cql_escape(col.name)
            out.write(",\n  %s %s" % (colname, cql_typename(col.validation_class)))
            if col.index_name is not None:
                indexed_columns.append(col)
        notable_columns = []
        for (option, thriftname) in cqlhandling.columnfamily_options:
            optval = getattr(cfdef, thriftname or option, None)
            if optval is None:
                continue
            if option in ('comparator', 'default_validation'):
                optval = cql_typename(optval)
            elif option == 'row_cache_provider':
                optval = cql_escape(trim_if_present(optval, 'org.apache.cassandra.cache.'))
            elif option == 'compaction_strategy_class':
                optval = cql_escape(trim_if_present(optval, 'org.apache.cassandra.db.compaction.'))
            else:
                optval = cql_escape(optval)
            notable_columns.append((option, optval))
        for option, thriftname, _ in cqlhandling.columnfamily_map_options:
            optmap = getattr(cfdef, thriftname or option, {})
            for k, v in optmap.items():
                notable_columns.append(('%s:%s' % (option, k), cql_escape(v)))
        out.write('\n)')
        if notable_columns:
            joiner = 'WITH'
            for optname, optval in notable_columns:
                out.write(" %s\n  %s=%s" % (joiner, optname, optval))
                joiner = 'AND'
        out.write(";\n")

        for col in indexed_columns:
            out.write('\n')
            # guess CQL can't represent index_type or index_options
            out.write('CREATE INDEX %s ON %s (%s);\n'
                         % (col.index_name, cfname, maybe_cql_escape(col.name)))

    def describe_keyspace(self, ksname):
        print
        self.print_recreate_keyspace(self.get_keyspace(ksname), sys.stdout)
        print

    def describe_columnfamily(self, cfname):
        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():
                print 'Keyspace %s' % (k.name,)
                print '---------%s\n' % ('-' * len(k.name))
                cmd.Cmd.columnize(self, [c.name for c in k.cf_defs])
                print
        else:
            names = self.get_columnfamily_names(ksname)
            print
            cmd.Cmd.columnize(self, names)
            print

    def describe_cluster(self):
        print 'Cluster: %s' % self.get_cluster_name()
        p = trim_if_present(self.get_partitioner(), 'org.apache.cassandra.dht.')
        print 'Partitioner: %s' % p
        snitch = trim_if_present(self.get_snitch(), 'org.apache.cassandra.locator.')
        print 'Snitch: %s\n' % snitch
        if self.current_keyspace is not None and self.current_keyspace != 'system':
            print "Range ownership:"
            ring = self.get_ring()
            for entry in ring:
                print ' %39s  [%s]' % (entry.start_token, ', '.join(entry.endpoints))
            print

    def describe_schema(self):
        print
        for k in self.get_keyspaces():
            self.print_recreate_keyspace(k, sys.stdout)
            print

    def do_describe(self, parsed):
        """
        DESCRIBE [cqlsh only]

        (DESC may be used as a shorthand.)

          Outputs information about the connected Cassandra cluster, or about
          the data stored on it. Use in one of the following ways:

        DESCRIBE KEYSPACE [<keyspacename>]

          Output CQL commands that could be used to recreate the given
          keyspace, and the columnfamilies in it. In some cases, as the CQL
          interface matures, there will be some metadata about a keyspace that
          is not representable with CQL. That metadata will not be shown.

          The '<keyspacename>' argument may be omitted when using a non-system
          keyspace; in that case, the current keyspace will be described.

        DESCRIBE COLUMNFAMILIES

          Output the names of all column families in the current keyspace, or
          in all keyspaces if there is no current keyspace.

        DESCRIBE COLUMNFAMILY <columnfamilyname>

          Output CQL commands that could be used to recreate the given
          columnfamily. In some cases, as above, there may be columnfamily
          metadata which is not representable and which will not be shown.

        DESCRIBE CLUSTER

          Output information about the connected Cassandra cluster, such as the
          cluster name, and the partitioner and snitch in use. When you are
          connected to a non-system keyspace, also shows endpoint-range
          ownership information for the Cassandra ring.

        DESCRIBE SCHEMA

          Output CQL commands that could be used to recreate the entire schema.
          Works as though "DESCRIBE KEYSPACE k" was invoked for each keyspace
          k.
        """

        what = parsed.matched[1][1].lower()
        if what == 'keyspace':
            ksname = cql_dequote(parsed.get_binding('ksname', ''))
            if not ksname:
                ksname = self.current_keyspace
                if ksname is None:
                    self.printerr('Not in any keyspace.')
                    return
            self.describe_keyspace(ksname)
        elif what == 'columnfamily':
            cfname = cql_dequote(parsed.get_binding('cfname'))
            self.describe_columnfamily(cfname)
        elif what == 'columnfamilies':
            self.describe_columnfamilies(self.current_keyspace)
        elif what == 'cluster':
            self.describe_cluster()
        elif what == 'schema':
            self.describe_schema()

    do_desc = do_describe

    def do_show(self, parsed):
        """
        SHOW [cqlsh only]

          Displays information about the current cqlsh session. Can be called in
          the following ways:

        SHOW VERSION

          Shows the version and build of the connected Cassandra instance, as
          well as the versions of the CQL spec and the Thrift protocol that
          the connected Cassandra instance understands.

        SHOW HOST

          Shows where cqlsh is currently connected.

        SHOW ASSUMPTIONS

          Outputs the current list of type assumptions as specified by the
          user. See the help for the ASSUME command for more information.
        """

        showwhat = parsed.get_binding('what').lower()
        if showwhat == 'version':
            self.show_version()
        elif showwhat == 'host':
            self.show_host()
        elif showwhat == 'assumptions':
            self.show_assumptions()
        else:
            self.printerr('Wait, how do I show %r?' % (showwhat,))

    def do_assume(self, parsed):
        """
        ASSUME [cqlsh only]

          Instruct cqlsh to consider certain column names or values to be of a
          specified type, even if that type information is not specified in
          the columnfamily's metadata. Data will be deserialized according to
          the given type, and displayed appropriately when retrieved.

          Use thus:

        ASSUME [<keyspace>.]<columnfamily> NAMES ARE <type>;

          Treat all column names in the given columnfamily as being of the
          given type.

        ASSUME [<keyspace>.]<columnfamily> VALUES ARE <type>;

          Treat all column values in the given columnfamily as being of the
          given type, unless there is more information about the specific
          column being deserialized. That is, a column-specific ASSUME will
          take precedence here, as will column-specific metadata in the
          columnfamily's definition.

        ASSUME [<keyspace>.]<columnfamily>(<colname>) VALUES ARE <type>;

          Treat all values in the given column in the given columnfamily as
          being of the specified type. This overrides any other information
          about the type of a value.

        Assign multiple overrides at once for the same columnfamily by
        separating with commas:

          ASSUME ks.cf NAMES ARE uuid, VALUES ARE int, (col) VALUES ARE ascii

        See HELP TYPES for information on the supported data storage types.
        """

        params = {}
        for paramname in ('ks', 'cf', 'colname', 'names', 'values', 'colvalues'):
            val = parsed.get_binding(paramname, None)
            if val is not None:
                val = cql_dequote(val)
            params[paramname] = val

        if params['ks'] is None:
            if self.current_keyspace is None:
                self.printerr('Error: not in any keyspace.')
                return
            params['ks'] = self.current_keyspace

        for overridetype in ('names', 'values', 'colvalues'):
            cqltype = params[overridetype]
            if cqltype is None:
                continue
            try:
                validator_class = cqlhandling.find_validator_class(cqltype)
            except KeyError:
                self.printerr('Error: validator type %s not found.' % cqltype)
            else:
                self.add_assumption(params['ks'], params['cf'], params['colname'],
                                    overridetype, validator_class)

    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, cqlver=self.cql_version)
        subshell.cmdloop()
        f.close()

    def do_capture(self, parsed):
        """
        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.

        Usage:

          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.
        """

        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

        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.
        """
        self.stop = True
    do_quit = do_exit

    def get_names(self):
        names = cmd.Cmd.get_names(self)
        for hide_from_help in ('do_quit',):
            names.remove(hide_from_help)
        return names

    def columnize(self, slist, *a, **kw):
        return cmd.Cmd.columnize(self, [u.upper() for u in slist], *a, **kw)

    def do_help(self, parsed):
        """
        HELP [cqlsh only]

        Gives information about cqlsh commands. To see available topics,
        enter "HELP" without any arguments. To see help on a topic,
        use "HELP <topic>".
        """
        topics = parsed.get_binding('topic', ())
        if not topics:
            return cmd.Cmd.do_help(self, '')
        for t in topics:
            cmd.Cmd.do_help(self, cql_dequote(t).lower())

    def help_types(self):
        print "\n        CQL types recognized by this version of cqlsh:\n"
        for t in cqlhandling.cql_types:
            print '          ' + t
        print """
        For information on the various recognizable input formats for these
        types, or on controlling the formatting of cqlsh query output, see
        one of the following topics:

          HELP TIMESTAMP_INPUT
          HELP BLOB_INPUT
          HELP UUID_INPUT
          HELP BOOLEAN_INPUT

          HELP TEXT_OUTPUT
          HELP TIMESTAMP_OUTPUT
        """

    def help_timestamp_input(self):
        print """
        Timestamp input

        CQL supports any of the following ISO 8601 formats for timestamp
        specification:

          yyyy-mm-dd HH:mm
          yyyy-mm-dd HH:mm:ss
          yyyy-mm-dd HH:mmZ
          yyyy-mm-dd HH:mm:ssZ
          yyyy-mm-dd'T'HH:mm
          yyyy-mm-dd'T'HH:mmZ
          yyyy-mm-dd'T'HH:mm:ss
          yyyy-mm-dd'T'HH:mm:ssZ
          yyyy-mm-dd
          yyyy-mm-ddZ

        The Z in these formats refers to an RFC-822 4-digit time zone,
        expressing the time zone's difference from UTC. For example, a
        timestamp in Pacific Standard Time might be given thus:

          2012-01-20 16:14:12-0800

        If no time zone is supplied, the current time zone for the Cassandra
        server node will be used.
        """

    def help_blob_input(self):
        print """
        Blob input

        CQL blob data must be specified in a string literal as hexidecimal
        data. Example: to store the ASCII values for the characters in the
        string "CQL", use '43514c'.
        """

    def help_uuid_input(self):
        print """
        UUID input

        UUIDs may be specified in CQL using 32 hexidecimal characters,
        split up using dashes in the standard UUID format:

          XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
        """

    def help_boolean_input(self):
        print """
        Boolean input

        CQL accepts the strings 'true' and 'false' (case insensitive)
        as input for boolean types.
        """

    def help_text_output(self):
        print """
        Textual output

        When control characters, or other characters which can't be encoded
        in your current locale, are found in values of 'text' or 'ascii'
        types, it will be shown as a backslash escape. If color is enabled,
        any such backslash escapes will be shown in a different color from
        the surrounding text.

        Unicode code points in your data will be output intact, if the
        encoding for your locale is capable of decoding them. If you prefer
        that non-ascii characters be shown with Python-style "\\uABCD"
        escape sequences, invoke cqlsh with an ASCII locale (for example,
        by setting the $LANG environment variable to "C").
        """

    help_ascii_output = help_text_output

    def help_timestamp_output(self):
        print """
        Timestamp output

        Cqlsh will display timestamps in this format:

          yyyy-mm-dd HH:mm:ssZ

        which is a format acceptable as CQL timestamp input as well. It is
        planned that cqlsh should allow the user to change that output format
        if desired, but that feature is not yet available.
        """

    def help_select_expr(self):
        print """
        SELECT: Specifying Columns

          SELECT [FIRST n] [REVERSED] name1, name2, name3 FROM ...
          SELECT [FIRST n] [REVERSED] name1..nameN FROM ...
          SELECT COUNT(*) FROM ...

        The SELECT expression determines which columns will appear in the
        results and takes the form of either a comma separated list of names,
        or a range. The range notation consists of a start and end column name
        separated by two periods (..). The set of columns returned for a
        range is start and end inclusive.

        The FIRST option accepts an integer argument and can be used to apply a
        limit to the number of columns returned per row.  When this limit is
        left unset, it defaults to 10,000 columns.

        The REVERSED option causes the sort order of the results to be
        reversed.

        It is worth noting that unlike the projection in a SQL SELECT, there is
        no guarantee that the results will contain all of the columns
        specified. This is because Cassandra is schema-less and there are no
        guarantees that a given column exists.

        When the COUNT aggregate function is specified as a column to fetch, a
        single row will be returned, with a single column named "count" whose
        value is the number of rows from the pre-aggregation resultset.

        Currently, COUNT is the only function supported by CQL.
        """

    def help_select_columnfamily(self):
        print """
        SELECT: Specifying Column Family

          SELECT ... FROM [<keyspace>.]<columnFamily> ...

        The FROM clause is used to specify the Cassandra column family
        applicable to a SELECT query. The keyspace in which the column family
        exists can optionally be specified along with the column family name,
        separated by a dot (.). This will not change the current keyspace of
        the session (see HELP USE).
        """

    def help_select_where(self):
        print """
        SELECT: Filtering rows

          SELECT ... WHERE <key> = keyname AND name1 = value1
          SELECT ... WHERE <key> >= startkey and <key> =< endkey AND name1 = value1
          SELECT ... WHERE <key> IN ('<key>', '<key>', '<key>', ...)

        The WHERE clause provides for filtering the rows that appear in
        results.  The clause can filter on a key name, or range of keys, and in
        the case of indexed columns, on column values.  Key filters are
        specified using the KEY keyword or key alias name, a relational
        operator (one of =, >, >=, <, and <=), and a term value.  When terms
        appear on both sides of a relational operator it is assumed the filter
        applies to an indexed column. With column index filters, the term on
        the left of the operator is the name, the term on the right is the
        value to filter _on_.

        Note: The greater-than and less-than operators (> and <) result in key
        ranges that are inclusive of the terms. There is no supported notion of
        "strictly" greater-than or less-than; these operators are merely
        supported as aliases to >= and <=.
        """

    def help_select_limit(self):
        print """
        SELECT: Limiting results

          SELECT ... WHERE <clause> [LIMIT n] ...

        Limiting the number of rows returned can be achieved by adding the
        LIMIT option to a SELECT expression. LIMIT defaults to 10,000 when left
        unset.
        """

    def help_consistencylevel(self):
        print """
        Consistency Level Specification

          ... USING CONSISTENCY <consistencylevel> ...

        Consistency level specifications are made up of keyword USING,
        followed by a consistency level identifier. Valid consistency level
        identifiers are as follows:

         * ANY
         * ONE (default)
         * QUORUM
         * ALL
         * LOCAL_QUORUM
         * EACH_QUORUM

        For more information on how consistency levels work, consult your
        Cassandra documentation.
        """

    def help_insert(self):
        print """
        INSERT INTO <columnFamily>
                    ( <keyname>, <colname1> [, <colname2> [, ...]] )
               VALUES ( <keyval>, <colval1> [, <colval2> [, ...]] )
               [USING CONSISTENCY <consistencylevel>
                 [AND TIMESTAMP <timestamp>]
                 [AND TTL <timeToLive]];

        An INSERT is used to write one or more columns to a record in a
        Cassandra column family. No results are returned.

        The first column name in the INSERT list must be the name of the
        column family key. Also, there must be more than one column name
        specified (Cassandra rows are not considered to exist with only
        a key and no associated columns).

        Unlike in SQL, the semantics of INSERT and UPDATE are identical.
        In either case a record is created if none existed before, and
        udpated when it does. For more information, see one of the
        following:

          HELP UPDATE
          HELP UPDATE_USING
          HELP CONSISTENCYLEVEL
        """

    def help_update(self):
        print """
        UPDATE <columnFamily> [USING CONSISTENCY <consistencylevel>
                                [AND TIMESTAMP <timestamp>]
                                [AND TTL <timeToLive>]]
               SET name1 = value1, name2 = value2 WHERE <KEY> = keyname;

        An UPDATE is used to write one or more columns to a record in a
        Cassandra column family. No results are returned. Key can be given
        using the KEY keyword or by an alias set per columnfamily.

        Statements begin with the UPDATE keyword followed by a Cassandra
        column family name.

        For more information, see one of the following:

          HELP UPDATE_USING
          HELP UPDATE_SET
          HELP UPDATE_COUNTERS
          HELP UPDATE_WHERE
          HELP CONSISTENCYLEVEL
        """

    def help_update_using(self):
        print """
        UPDATE: the USING clause

          UPDATE ... USING TIMESTAMP <timestamp>;
          UPDATE ... USING TTL <timeToLive>;
          UPDATE ... USING CONSISTENCY <consistencylevel>;

        The USING clause allows setting of certain query and data parameters.
        If multiple parameters need to be set, these may be joined using AND.
        Example:

          UPDATE ... USING TTL 43200 AND CONSISTENCY LOCAL_QUORUM;

        <timestamp> defines the optional timestamp for the new column value(s).
        It must be an integer.

        <timeToLive> defines the optional time to live (TTL) for the new column
        value(s). It must be an integer.
        """

    def help_update_set(self):
        print """
        UPDATE: Specifying Columns and Row

          UPDATE ... SET name1 = value1, name2 = value2
                   WHERE <key> = keyname;
          UPDATE ... SET name1 = value1, name2 = value2
                   WHERE <key> IN ('<key1>', '<key2>', ...)

        Rows are created or updated by supplying column names and values in
        term assignment format.  Multiple columns can be set by separating the
        name/value pairs using commas.
        """

    def help_update_counters(self):
        print """
        UPDATE: Updating Counter Columns

          UPDATE ... SET name1 = name1 + <value> ...
          UPDATE ... SET name1 = name1 - <value> ...

        Counter columns can be incremented or decremented by an arbitrary
        numeric value though the assignment of an expression that adds or
        substracts the value.
        """

    def help_update_where(self):
        print """
        UPDATE: Selecting rows to update

          UPDATE ... WHERE <keyname> = <keyval>;
          UPDATE ... WHERE <keyname> IN (<keyval1>, <keyval2>, ...);

        Each update statement requires a precise set of keys to be specified
        using a WHERE clause.

        <keyname> can be the keyword KEY or the key alias for the column
        family.
        """

    def help_delete(self):
        print """
        DELETE [<col1> [, <col2>, ...] FROM <columnFamily>
               [USING CONSISTENCY <consistencylevel>
                   [AND TIMESTAMP <timestamp>]]
            WHERE <keyname> = <keyvalue>;

        A DELETE is used to perform the removal of one or more columns from one
        or more rows. Each DELETE statement requires a precise set of row keys
        to be specified using a WHERE clause and the KEY keyword or key alias.

        For more information, see one of the following:

          HELP DELETE_USING
          HELP DELETE_COLUMNS
          HELP DELETE_WHERE
          HELP CONSISTENCYLEVEL
        """

    def help_delete_using(self):
        print """
        DELETE: the USING clause

          DELETE ... USING CONSISTENCY <consistencylevel>;
          DELETE ... USING TIMESTAMP <timestamp>;

        The USING clause allows setting of certain query and data parameters.
        If multiple parameters need to be set, these may be joined using AND.
        Example:

          DELETE ... CONSISTENCY LOCAL_QUORUM AND TIMESTAMP 1318452291034;

        <timestamp> defines the optional timestamp for the new tombstone
        record. It must be an integer.
        """

    def help_delete_columns(self):
        print """
        DELETE: specifying columns

          DELETE col1, 'col2 name', 3 FROM ...

        Following the DELETE keyword is an optional comma-delimited list of
        column name terms. When no column names are given, the remove applies
        to the entire row(s) matched by the WHERE clause.
        """

    def help_delete_where(self):
        print """
        DELETE: specifying rows

          DELETE ... WHERE KEY = 'some_key_value';
          DELETE ... WHERE keyalias IN (key1, key2);

        The WHERE clause is used to determine to which row(s) a DELETE
        applies. The first form allows the specification of a single keyname
        using the KEY keyword (or the key alias) and the = operator. The
        second form allows a list of keyname terms to be specified using the
        IN operator and a parenthesized list of comma-delimited keyname
        terms.
        """

    def help_create(self):
        print """
        There are different variants of CREATE. For more information, see
        one of the following:

          HELP CREATE_KEYSPACE;
          HELP CREATE_COLUMNFAMILY;
          HELP CREATE_INDEX;
        """

    def help_create_keyspace(self):
        print """
        CREATE KEYSPACE <ksname> WITH strategy_class = '<strategy>'
                                 [AND strategy_options:<option> = <val>];

        The CREATE KEYSPACE statement creates a new top-level namespace (aka
        "keyspace"). Valid names are any string constructed of alphanumeric
        characters and underscores. Names which do not work as valid
        identifiers or integers should be quoted as string literals. Properties
        such as replication strategy and count are specified during creation
        using the following accepted keyword arguments:

          strategy_class [required]: The name of the replication strategy class
          which should be used for the new keyspace. Some often-used classes
          are SimpleStrategy and NetworkTopologyStrategy.

          strategy_options: Most strategies require additional arguments which
          can be supplied by appending the option name to "strategy_options",
          separated by a colon (:). For example, a strategy option of "DC1"
          with a value of "1" would be specified as "strategy_options:DC1 = 1".
          The replication factor option for SimpleStrategy could be
          "strategy_options:replication_factor=3".
        """

    def help_create_columnfamily(self):
        print """
        CREATE COLUMNFAMILY <cfname> ( <colname> <type> PRIMARY KEY [,
                                       <colname> <type> [, ...]] )
               [WITH <optionname> = <val> [AND <optionname> = <val> [...]]];

        CREATE COLUMNFAMILY statements create a new column family under the
        current keyspace. Valid column family names are strings of alphanumeric
        characters and underscores, which begin with a letter.

        Each columnfamily requires at least one column definition and type,
        which will correspond to the columnfamily key and key validator. It's
        important to note that the key type you use must be compatible with the
        partitioner in use. For example, OrderPreservingPartitioner and
        CollatingOrderPreservingPartitioner both require UTF-8 keys. If you
        use an identifier for the primary key name, instead of the KEY
        keyword, a key alias will be set automatically.

        For more information, see one of the following:

          HELP CREATE_COLUMNFAMILY_TYPES;
          HELP CREATE_COLUMNFAMILY_OPTIONS;
        """

    def help_create_columnfamily_types(self):
        print """
        CREATE COLUMNFAMILY: Specifying column types

          CREATE ... (KEY <type> PRIMARY KEY,
                      othercol <type>) ...

        It is possible to assign columns a type during column family creation.
        Columns configured with a type are validated accordingly when a write
        occurs, and intelligent CQL drivers and interfaces will be able to
        decode the column values correctly when receiving them. Column types
        are specified as a parenthesized, comma-separated list of column term
        and type pairs. See HELP TYPES; for the list of recognized types.
        """

    def help_create_columnfamily_options(self):
        print """
        CREATE COLUMNFAMILY: Specifying columnfamily options

          CREATE COLUMNFAMILY blah (...)
             WITH optionname = val AND otheroption = val2;

        A number of optional keyword arguments can be supplied to control the
        configuration of a new column family, such as the size of the
        associated row and key caches. Consult your CQL reference for the
        complete list of options and possible values.
        """

    def help_create_index(self):
        print """
        CREATE INDEX [<indexname>] ON <cfname> ( <colname> );

        A CREATE INDEX statement is used to create a new, automatic secondary
        index on the given column family, for the named column. A name for the
        index itself can be specified before the ON keyword, if desired. A
        single column name must be specified inside the parentheses. It is not
        necessary for the column to exist on any current rows (Cassandra is
        schemaless), but the column must already have a type (specified during
        the CREATE COLUMNFAMILY, or added afterwards with ALTER COLUMNFAMILY.
        """

    def help_drop(self):
        print """
        There are different variants of DROP. For more information, see
        one of the following:

          HELP DROP_KEYSPACE;
          HELP DROP_COLUMNFAMILY;
          HELP DROP_INDEX;
        """

    def help_drop_keyspace(self):
        print """
        DROP KEYSPACE <keyspacename>;

        A DROP KEYSPACE statement results in the immediate, irreversible
        removal of a keyspace, including all column families in it, and all
        data contained in those column families.
        """

    def help_drop_columnfamily(self):
        print """
        DROP COLUMNFAMILY <columnfamilyname>;

        A DROP COLUMNFAMILY statement results in the immediate, irreversible
        removal of a column family, including all data contained in it.
        """

    def help_drop_index(self):
        print """
        DROP INDEX <indexname>;

        A DROP INDEX statement is used to drop an existing secondary index.
        """

    def help_truncate(self):
        print """
        TRUNCATE <columnfamilyname>;

        TRUNCATE accepts a single argument for the column family name, and
        permanently removes all data from said column family.
        """

    def help_begin(self):
        print """
        BEGIN BATCH [USING CONSISTENCY <level>
                       [AND TIMESTAMP <timestamp>]]
          <insert or update or delete statement> ;
          [ <another insert or update or delete statement ;
            [...]]
        APPLY BATCH;

        BATCH supports setting a client-supplied optional global timestamp
        which will be used for each of the operations included in the batch.

        A single consistency level is used for the entire batch. It appears
        after the BEGIN BATCH statement, and uses the standard "consistency
        level specification" (see HELP CONSISTENCYLEVEL). Batched statements
        default to CONSISTENCY.ONE when left unspecified.

        Only data modification statements (specifically, UPDATE, INSERT,
        and DELETE) are allowed in a BATCH statement. BATCH is _not_ an
        analogue for SQL transactions.

        _NOTE: While there are no isolation guarantees, UPDATE queries are
        atomic within a given record._
        """
    help_apply = help_begin

    def help_alter(self):
        print """
        ALTER COLUMNFAMILY <cfname> ALTER <columnname> TYPE <type>;
        ALTER COLUMNFAMILY <cfname> ADD <columnname> <type>;
        ALTER COLUMNFAMILY <cfname> DROP <columnname>;
        ALTER COLUMNFAMILY <cfname> WITH <optionname> = <val> [AND <optionname> = <val> [...]];

        An ALTER statement is used to manipulate column family column
        metadata. It allows you to add new columns, drop existing columns,
        change the data storage type of existing columns, or change column
        family properties. No results are returned.

        See one of the following for more information:

          HELP ALTER_ALTER;
          HELP ALTER_ADD;
          HELP ALTER_DROP;
          HELP ALTER_WITH;
        """

    def help_alter_alter(self):
        print """
        ALTER COLUMNFAMILY: altering existing typed columns

          ALTER COLUMNFAMILY addamsFamily ALTER lastKnownLocation TYPE uuid;

        ALTER COLUMNFAMILY ... ALTER changes the expected storage type for a
        column. The column must either be the key alias or already have a type
        in the column family metadata. The column may or may not already exist
        in current rows-- but be aware that no validation of existing data is
        done. The bytes stored in values for that column will remain unchanged,
        and if existing data is not deserializable according to the new type,
        this may cause your CQL driver or interface to report errors.
        """

    def help_alter_add(self):
        print """
        ALTER COLUMNFAMILY: adding a typed column

          ALTER COLUMNFAMILY addamsFamily ADD gravesite varchar;

        The ALTER COLUMNFAMILY ... ADD variant adds a typed column to a column
        family. The column must not already have a type in the column family
        metadata. See the warnings on HELP ALTER_ALTER regarding the lack of
        validation of existing data; they apply here as well.
        """

    def help_alter_drop(self):
        print """
        ALTER COLUMNFAMILY: dropping a typed column

          ALTER COLUMNFAMILY addamsFamily DROP gender;

        An ALTER COLUMNFAMILY ... DROP statement removes the type of a column
        from the column family metadata. Note that this does _not_ remove the
        column from current rows; it just removes the metadata saying that the
        bytes stored under that column are expected to be deserializable
        according to a certain type.
        """

    def help_alter_with(self):
        print """
        ALTER COLUMNFAMILY: changing column family properties

          ALTER COLUMNFAMILY addamsFamily WITH comment = 'Glad to be here!'
                                           AND read_repair_chance = 0.2;

        An ALTER COLUMNFAMILY ... WITH statement makes adjustments to the
        column family properties, as defined when the column family was created
        (see HELP CREATE_COLUMNFAMILY_OPTIONS, and your Cassandra documentation
        for information about the supported parameter names and values).
        """

    def applycolor(self, text, color=None):
        if not color or not self.color:
            return text
        return color + text + ANSI_RESET

    def writeresult(self, text, color=None, newline=True, out=None):
        if out is None:
            out = self.query_out
        out.write(self.applycolor(str(text), color) + ('\n' if newline else ''))

    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:
            v_info = self.schema_overrides[(ksname, cfname)]
        except KeyError:
            v_info = self.schema_overrides[(ksname, cfname)] = FakeCqlMetadata()
        if valtype == 'names':
            v_info.default_name_type = valclass
        elif valtype == 'values':
            v_info.default_value_type = valclass
        elif valtype == 'colvalues':
            v_info.value_types[colname] = valclass


class FakeCqlMetadata:
    def __init__(self):
        self.name_types = {}
        self.value_types = {}
        self.default_name_type = None
        self.default_value_type = None

class OverrideableSchemaDecoder(cql.decoders.SchemaDecoder):
    def __init__(self, schema, overrides=None):
        cql.decoders.SchemaDecoder.__init__(self, schema)
        self.apply_schema_overrides(overrides)

    def apply_schema_overrides(self, overrides):
        if overrides is None:
            return
        if overrides.default_name_type is not None:
            self.schema.default_name_type = overrides.default_name_type
        if overrides.default_value_type is not None:
            self.schema.default_value_type = overrides.default_value_type
        self.schema.name_types.update(overrides.name_types)
        self.schema.value_types.update(overrides.value_types)

class ErrorHandlingSchemaDecoder(OverrideableSchemaDecoder):
    def name_decode_error(self, err, namebytes, expectedtype):
        return DecodeError(namebytes, err, expectedtype)

    def value_decode_error(self, err, namebytes, valuebytes, expectedtype):
        return DecodeError(valuebytes, err, expectedtype, colname=namebytes)


def option_with_default(cparser_getter, section, option, default=None):
    try:
        return cparser_getter(section, option)
    except ConfigParser.Error:
        return default

def should_use_color():
    if not sys.stdout.isatty():
        return False
    if os.environ.get('TERM', 'dumb') == 'dumb':
        return False
    try:
        import subprocess
        p = subprocess.Popen(['tput', 'colors'], stdout=subprocess.PIPE)
        stdout, _ = p.communicate()
        if int(stdout.strip()) < 8:
            return False
    except (OSError, ImportError):
        # oh well, we tried. at least we know there's a $TERM and it's
        # not "dumb".
        pass
    return True

def read_options(cmdlineargs, environment):
    configs = ConfigParser.SafeConfigParser()
    configs.read(CONFIG_FILE)

    optvalues = optparse.Values()
    optvalues.username = option_with_default(configs.get, 'authentication', 'username')
    optvalues.password = option_with_default(configs.get, 'authentication', 'password')
    optvalues.completekey = option_with_default(configs.get, 'ui', 'completekey', 'tab')
    optvalues.color = option_with_default(configs.getboolean, 'ui', 'color')
    if optvalues.color is None:
        # default yes if tty
        optvalues.color = should_use_color()
    optvalues.debug = False
    optvalues.file = None
    optvalues.tty = sys.stdin.isatty()
    optvalues.cqlversion = option_with_default(configs.get, 'cql', 'version', DEFAULT_CQLVER)

    (options, arguments) = parser.parse_args(cmdlineargs, values=optvalues)

    hostname = option_with_default(configs.get, 'connection', 'hostname', DEFAULT_HOST)
    port = option_with_default(configs.get, 'connection', 'port', DEFAULT_PORT)

    hostname = environment.get('CQLSH_HOST', hostname)
    port = environment.get('CQLSH_PORT', port)

    if len(arguments) > 0:
        hostname = arguments[0]
    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:
        parser.error('%r is not a valid port number.' % port)

    return options, hostname, port

def main(options, hostname, port):
    if os.path.exists(HISTORY) and readline is not None:
        readline.read_history_file(HISTORY)
        delims = readline.get_completer_delims()
        delims.replace("'", "")
        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))

    if options.debug:
        import thrift
        sys.stderr.write("Using CQL driver: %s\n" % (cql,))
        sys.stderr.write("Using thrift lib: %s\n" % (thrift,))

    try:
        shell = Shell(hostname,
                      port,
                      color=options.color,
                      username=options.username,
                      password=options.password,
                      stdin=stdin,
                      tty=options.tty,
                      completekey=options.completekey,
                      cqlver=options.cqlversion)
    except KeyboardInterrupt:
        sys.exit('Connection aborted.')
    except CQL_ERRORS, e:
        sys.exit('Connection error: %s' % (e,))
    except VersionNotSupported, e:
        sys.exit('Unsupported CQL version: %s' % (e,))
    if options.debug:
        shell.debug = True

    shell.cmdloop()

    if readline is not None:
        readline.write_history_file(HISTORY)

if __name__ == '__main__':
    main(*read_options(sys.argv[1:], os.environ))

# vim: set ft=python et ts=4 sw=4 :
