2014-10-12 17:31:54 +00:00
|
|
|
#!/usr/bin/env python
|
|
|
|
from __future__ import unicode_literals
|
2014-11-21 07:15:50 +00:00
|
|
|
from __future__ import print_function
|
2014-10-12 22:07:34 +00:00
|
|
|
|
2014-12-20 07:12:43 +00:00
|
|
|
import os
|
2015-10-18 20:55:56 +00:00
|
|
|
import re
|
2015-02-14 01:02:24 +00:00
|
|
|
import sys
|
2015-01-06 21:52:01 +00:00
|
|
|
import traceback
|
2015-01-04 08:31:17 +00:00
|
|
|
import logging
|
2015-08-28 06:10:48 +00:00
|
|
|
import threading
|
2015-09-20 21:57:12 +00:00
|
|
|
import shutil
|
2015-01-31 21:46:32 +00:00
|
|
|
from time import time
|
2015-08-17 04:34:27 +00:00
|
|
|
from codecs import open
|
2014-10-12 17:31:54 +00:00
|
|
|
|
2015-01-31 21:46:32 +00:00
|
|
|
import click
|
2015-10-23 09:49:20 +00:00
|
|
|
try:
|
|
|
|
import setproctitle
|
|
|
|
except ImportError:
|
|
|
|
setproctitle = None
|
2015-01-31 21:46:32 +00:00
|
|
|
import sqlparse
|
2015-05-31 13:59:20 +00:00
|
|
|
from prompt_toolkit import CommandLineInterface, Application, AbortAction
|
|
|
|
from prompt_toolkit.enums import DEFAULT_BUFFER
|
2015-05-02 15:01:57 +00:00
|
|
|
from prompt_toolkit.shortcuts import create_default_layout, create_eventloop
|
2015-01-24 03:19:07 +00:00
|
|
|
from prompt_toolkit.document import Document
|
2015-05-31 13:59:20 +00:00
|
|
|
from prompt_toolkit.filters import Always, HasFocus, IsDone
|
2015-06-04 23:55:49 +00:00
|
|
|
from prompt_toolkit.layout.processors import (ConditionalProcessor,
|
|
|
|
HighlightMatchingBracketProcessor)
|
2014-11-23 23:31:34 +00:00
|
|
|
from prompt_toolkit.history import FileHistory
|
2015-03-27 04:20:20 +00:00
|
|
|
from pygments.lexers.sql import PostgresLexer
|
2015-05-02 15:01:57 +00:00
|
|
|
from pygments.token import Token
|
2014-12-05 16:56:59 +00:00
|
|
|
|
2014-12-08 08:43:21 +00:00
|
|
|
from .packages.tabulate import tabulate
|
2015-01-07 10:21:40 +00:00
|
|
|
from .packages.expanded import expanded_table
|
2015-09-28 05:36:21 +00:00
|
|
|
from pgspecial.main import (PGSpecial, NO_QUERY, content_exceeds_width)
|
2015-09-21 23:52:36 +00:00
|
|
|
import pgspecial as special
|
2014-12-05 16:56:59 +00:00
|
|
|
from .pgcompleter import PGCompleter
|
2015-05-02 15:01:57 +00:00
|
|
|
from .pgtoolbar import create_toolbar_tokens_func
|
2015-02-04 07:31:35 +00:00
|
|
|
from .pgstyle import style_factory
|
2015-10-21 20:56:11 +00:00
|
|
|
from .pgexecute import PGExecute
|
2015-01-08 20:07:08 +00:00
|
|
|
from .pgbuffer import PGBuffer
|
2015-09-04 05:37:31 +00:00
|
|
|
from .completion_refresher import CompletionRefresher
|
2015-09-20 21:57:12 +00:00
|
|
|
from .config import write_default_config, load_config, config_location
|
2014-12-11 08:26:32 +00:00
|
|
|
from .key_bindings import pgcli_bindings
|
2015-01-27 07:07:10 +00:00
|
|
|
from .encodingutils import utf8tounicode
|
2015-03-01 05:37:59 +00:00
|
|
|
from .__init__ import __version__
|
2014-10-12 17:31:54 +00:00
|
|
|
|
2015-08-18 16:11:22 +00:00
|
|
|
click.disable_unicode_literals_warning = True
|
2015-01-16 07:41:43 +00:00
|
|
|
|
2015-01-09 08:24:17 +00:00
|
|
|
try:
|
|
|
|
from urlparse import urlparse
|
|
|
|
except ImportError:
|
|
|
|
from urllib.parse import urlparse
|
2015-08-18 16:11:22 +00:00
|
|
|
|
2015-01-07 21:50:48 +00:00
|
|
|
from getpass import getuser
|
2015-02-08 22:54:57 +00:00
|
|
|
from psycopg2 import OperationalError
|
2015-01-07 21:50:48 +00:00
|
|
|
|
2015-01-16 07:41:43 +00:00
|
|
|
from collections import namedtuple
|
2015-01-09 00:44:24 +00:00
|
|
|
|
2015-01-16 07:41:43 +00:00
|
|
|
# Query tuples are used for maintaining history
|
2015-10-28 13:23:23 +00:00
|
|
|
MetaQuery = namedtuple(
|
|
|
|
'Query',
|
|
|
|
[
|
|
|
|
'query', # The entire text of the command
|
|
|
|
'successful', # True If all subqueries were successful
|
|
|
|
'total_time', # Time elapsed executing the query
|
|
|
|
'meta_changed', # True if any subquery executed create/alter/drop
|
|
|
|
'db_changed', # True if any subquery changed the database
|
|
|
|
'path_changed', # True if any subquery changed the search path
|
|
|
|
'mutated', # True if any subquery executed insert/update/delete
|
|
|
|
])
|
|
|
|
MetaQuery.__new__.__defaults__ = ('', False, 0, False, False, False, False)
|
2015-08-06 05:36:45 +00:00
|
|
|
|
2015-10-19 17:40:50 +00:00
|
|
|
|
2015-01-09 00:27:28 +00:00
|
|
|
class PGCli(object):
|
2015-08-06 05:36:45 +00:00
|
|
|
|
2015-01-09 00:27:28 +00:00
|
|
|
def __init__(self, force_passwd_prompt=False, never_passwd_prompt=False,
|
2015-08-06 05:36:45 +00:00
|
|
|
pgexecute=None, pgclirc_file=None):
|
2015-01-09 00:27:28 +00:00
|
|
|
|
|
|
|
self.force_passwd_prompt = force_passwd_prompt
|
|
|
|
self.never_passwd_prompt = never_passwd_prompt
|
|
|
|
self.pgexecute = pgexecute
|
|
|
|
|
|
|
|
from pgcli import __file__ as package_root
|
|
|
|
package_root = os.path.dirname(package_root)
|
|
|
|
|
|
|
|
default_config = os.path.join(package_root, 'pgclirc')
|
2015-08-06 05:36:45 +00:00
|
|
|
write_default_config(default_config, pgclirc_file)
|
2015-01-09 00:27:28 +00:00
|
|
|
|
2015-06-25 10:27:29 +00:00
|
|
|
self.pgspecial = PGSpecial()
|
|
|
|
|
2015-01-09 00:27:28 +00:00
|
|
|
# Load config.
|
2015-08-06 05:36:45 +00:00
|
|
|
c = self.config = load_config(pgclirc_file, default_config)
|
2015-05-28 11:59:18 +00:00
|
|
|
self.multi_line = c['main'].as_bool('multi_line')
|
|
|
|
self.vi_mode = c['main'].as_bool('vi')
|
2015-06-30 21:59:02 +00:00
|
|
|
self.pgspecial.timing_enabled = c['main'].as_bool('timing')
|
2015-05-28 11:59:18 +00:00
|
|
|
self.table_format = c['main']['table_format']
|
|
|
|
self.syntax_style = c['main']['syntax_style']
|
2015-08-07 04:14:49 +00:00
|
|
|
self.cli_style = c['colors']
|
2015-08-02 23:25:05 +00:00
|
|
|
self.wider_completion_menu = c['main'].as_bool('wider_completion_menu')
|
2015-10-01 12:15:38 +00:00
|
|
|
|
2015-10-21 20:56:11 +00:00
|
|
|
self.on_error = c['main']['on_error'].upper()
|
2015-10-01 12:15:38 +00:00
|
|
|
|
2015-09-04 05:37:31 +00:00
|
|
|
self.completion_refresher = CompletionRefresher()
|
2015-01-09 00:27:28 +00:00
|
|
|
|
|
|
|
self.logger = logging.getLogger(__name__)
|
|
|
|
self.initialize_logging()
|
|
|
|
|
2015-01-09 00:44:24 +00:00
|
|
|
self.query_history = []
|
|
|
|
|
2015-01-24 03:18:40 +00:00
|
|
|
# Initialize completer
|
2015-05-28 11:59:18 +00:00
|
|
|
smart_completion = c['main'].as_bool('smart_completion')
|
2015-07-06 11:20:25 +00:00
|
|
|
completer = PGCompleter(smart_completion, pgspecial=self.pgspecial)
|
2015-01-24 03:18:40 +00:00
|
|
|
self.completer = completer
|
2015-09-01 05:07:32 +00:00
|
|
|
self._completer_lock = threading.Lock()
|
2015-06-08 16:51:03 +00:00
|
|
|
self.register_special_commands()
|
|
|
|
|
2015-09-05 15:05:30 +00:00
|
|
|
self.cli = None
|
|
|
|
|
2015-06-08 16:51:03 +00:00
|
|
|
def register_special_commands(self):
|
2015-06-25 10:27:29 +00:00
|
|
|
|
|
|
|
self.pgspecial.register(self.change_db, '\\c',
|
|
|
|
'\\c[onnect] database_name',
|
|
|
|
'Change to a new database.',
|
|
|
|
aliases=('use', '\\connect', 'USE'))
|
|
|
|
self.pgspecial.register(self.refresh_completions, '\\#', '\\#',
|
|
|
|
'Refresh auto-completions.', arg_type=NO_QUERY)
|
|
|
|
self.pgspecial.register(self.refresh_completions, '\\refresh', '\\refresh',
|
|
|
|
'Refresh auto-completions.', arg_type=NO_QUERY)
|
2015-08-17 04:34:27 +00:00
|
|
|
self.pgspecial.register(self.execute_from_file, '\\i', '\\i filename',
|
|
|
|
'Execute commands from file.')
|
2015-06-25 10:27:29 +00:00
|
|
|
|
2015-06-08 09:07:55 +00:00
|
|
|
def change_db(self, pattern, **_):
|
2015-06-18 02:46:46 +00:00
|
|
|
if pattern:
|
|
|
|
db = pattern[1:-1] if pattern[0] == pattern[-1] == '"' else pattern
|
2015-06-17 15:08:26 +00:00
|
|
|
self.pgexecute.connect(database=db)
|
2015-06-08 09:07:55 +00:00
|
|
|
else:
|
2015-06-18 02:46:46 +00:00
|
|
|
self.pgexecute.connect()
|
2015-06-08 09:07:55 +00:00
|
|
|
|
|
|
|
yield (None, None, None, 'You are now connected to database "%s" as '
|
|
|
|
'user "%s"' % (self.pgexecute.dbname, self.pgexecute.user))
|
2015-01-24 03:18:40 +00:00
|
|
|
|
2015-08-17 04:34:27 +00:00
|
|
|
def execute_from_file(self, pattern, **_):
|
|
|
|
if not pattern:
|
|
|
|
message = '\\i: missing required argument'
|
|
|
|
return [(None, None, None, message)]
|
|
|
|
try:
|
|
|
|
with open(os.path.expanduser(pattern), encoding='utf-8') as f:
|
|
|
|
query = f.read()
|
|
|
|
except IOError as e:
|
|
|
|
return [(None, None, None, str(e))]
|
|
|
|
|
2015-10-01 12:15:38 +00:00
|
|
|
return self.pgexecute.run(query, self.pgspecial, on_error=self.on_error)
|
2015-08-17 04:34:27 +00:00
|
|
|
|
2015-01-09 00:27:28 +00:00
|
|
|
def initialize_logging(self):
|
|
|
|
|
2015-05-28 23:06:55 +00:00
|
|
|
log_file = self.config['main']['log_file']
|
2015-11-05 02:21:59 +00:00
|
|
|
if log_file == 'default':
|
|
|
|
log_file = config_location() + 'log'
|
2015-05-28 23:06:55 +00:00
|
|
|
log_level = self.config['main']['log_level']
|
2015-01-09 00:27:28 +00:00
|
|
|
|
|
|
|
level_map = {'CRITICAL': logging.CRITICAL,
|
|
|
|
'ERROR': logging.ERROR,
|
|
|
|
'WARNING': logging.WARNING,
|
|
|
|
'INFO': logging.INFO,
|
|
|
|
'DEBUG': logging.DEBUG
|
|
|
|
}
|
|
|
|
|
|
|
|
handler = logging.FileHandler(os.path.expanduser(log_file))
|
|
|
|
|
|
|
|
formatter = logging.Formatter(
|
|
|
|
'%(asctime)s (%(process)d/%(threadName)s) '
|
|
|
|
'%(name)s %(levelname)s - %(message)s')
|
|
|
|
|
|
|
|
handler.setFormatter(formatter)
|
2015-01-08 12:58:44 +00:00
|
|
|
|
2015-01-10 23:01:14 +00:00
|
|
|
root_logger = logging.getLogger('pgcli')
|
|
|
|
root_logger.addHandler(handler)
|
|
|
|
root_logger.setLevel(level_map[log_level.upper()])
|
2015-01-09 00:27:28 +00:00
|
|
|
|
2015-01-10 23:01:14 +00:00
|
|
|
root_logger.debug('Initializing pgcli logging.')
|
2015-01-16 07:41:43 +00:00
|
|
|
root_logger.debug('Log file %r.', log_file)
|
2015-01-09 00:27:28 +00:00
|
|
|
|
2015-08-07 23:32:39 +00:00
|
|
|
def connect_dsn(self, dsn):
|
|
|
|
self.connect(dsn=dsn)
|
|
|
|
|
2015-01-09 00:27:28 +00:00
|
|
|
def connect_uri(self, uri):
|
|
|
|
uri = urlparse(uri)
|
|
|
|
database = uri.path[1:] # ignore the leading fwd slash
|
|
|
|
self.connect(database, uri.hostname, uri.username,
|
|
|
|
uri.port, uri.password)
|
|
|
|
|
2015-08-07 23:32:39 +00:00
|
|
|
def connect(self, database='', host='', user='', port='', passwd='',
|
|
|
|
dsn=''):
|
2015-01-09 00:27:28 +00:00
|
|
|
# Connect to the database.
|
|
|
|
|
2015-06-05 00:26:02 +00:00
|
|
|
if not user:
|
|
|
|
user = getuser()
|
|
|
|
|
2015-01-09 00:27:28 +00:00
|
|
|
if not database:
|
2015-06-05 00:26:02 +00:00
|
|
|
database = user
|
2015-01-09 00:27:28 +00:00
|
|
|
|
2015-06-23 02:23:35 +00:00
|
|
|
# If password prompt is not forced but no password is provided, try
|
|
|
|
# getting it from environment variable.
|
|
|
|
if not self.force_passwd_prompt and not passwd:
|
|
|
|
passwd = os.environ.get('PGPASSWORD', '')
|
|
|
|
|
2015-01-09 00:27:28 +00:00
|
|
|
# Prompt for a password immediately if requested via the -W flag. This
|
|
|
|
# avoids wasting time trying to connect to the database and catching a
|
|
|
|
# no-password exception.
|
|
|
|
# If we successfully parsed a password from a URI, there's no need to
|
|
|
|
# prompt for it, even with the -W flag
|
|
|
|
if self.force_passwd_prompt and not passwd:
|
|
|
|
passwd = click.prompt('Password', hide_input=True,
|
|
|
|
show_default=False, type=str)
|
|
|
|
|
|
|
|
# Prompt for a password after 1st attempt to connect without a password
|
|
|
|
# fails. Don't prompt if the -w flag is supplied
|
|
|
|
auto_passwd_prompt = not passwd and not self.never_passwd_prompt
|
|
|
|
|
|
|
|
# Attempt to connect to the database.
|
|
|
|
# Note that passwd may be empty on the first attempt. If connection
|
|
|
|
# fails because of a missing password, but we're allowed to prompt for
|
|
|
|
# a password (no -w flag), prompt for a passwd and try again.
|
|
|
|
try:
|
|
|
|
try:
|
2015-08-07 23:32:39 +00:00
|
|
|
pgexecute = PGExecute(database, user, passwd, host, port, dsn)
|
2015-01-09 00:27:28 +00:00
|
|
|
except OperationalError as e:
|
2015-01-27 07:07:10 +00:00
|
|
|
if ('no password supplied' in utf8tounicode(e.args[0]) and
|
|
|
|
auto_passwd_prompt):
|
2015-01-09 00:27:28 +00:00
|
|
|
passwd = click.prompt('Password', hide_input=True,
|
|
|
|
show_default=False, type=str)
|
2015-08-07 23:32:39 +00:00
|
|
|
pgexecute = PGExecute(database, user, passwd, host, port,
|
|
|
|
dsn)
|
2015-01-09 00:27:28 +00:00
|
|
|
else:
|
|
|
|
raise e
|
|
|
|
|
|
|
|
except Exception as e: # Connecting to a database could fail.
|
|
|
|
self.logger.debug('Database connection failed: %r.', e)
|
2015-01-27 07:07:10 +00:00
|
|
|
self.logger.error("traceback: %r", traceback.format_exc())
|
2015-01-09 00:27:28 +00:00
|
|
|
click.secho(str(e), err=True, fg='red')
|
|
|
|
exit(1)
|
|
|
|
|
|
|
|
self.pgexecute = pgexecute
|
|
|
|
|
2015-04-23 18:25:27 +00:00
|
|
|
def handle_editor_command(self, cli, document):
|
|
|
|
"""
|
|
|
|
Editor command is any query that is prefixed or suffixed
|
|
|
|
by a '\e'. The reason for a while loop is because a user
|
|
|
|
might edit a query multiple times.
|
|
|
|
For eg:
|
|
|
|
"select * from \e"<enter> to edit it in vim, then come
|
|
|
|
back to the prompt with the edited query "select * from
|
|
|
|
blah where q = 'abc'\e" to edit it again.
|
|
|
|
:param cli: CommandLineInterface
|
|
|
|
:param document: Document
|
|
|
|
:return: Document
|
|
|
|
"""
|
2015-06-04 23:55:49 +00:00
|
|
|
while special.editor_command(document.text):
|
|
|
|
filename = special.get_filename(document.text)
|
|
|
|
sql, message = special.open_external_editor(filename,
|
2015-04-23 18:25:27 +00:00
|
|
|
sql=document.text)
|
|
|
|
if message:
|
|
|
|
# Something went wrong. Raise an exception and bail.
|
|
|
|
raise RuntimeError(message)
|
2015-05-24 21:22:17 +00:00
|
|
|
cli.current_buffer.document = Document(sql, cursor_position=len(sql))
|
2015-05-31 13:59:20 +00:00
|
|
|
document = cli.run(False)
|
2015-04-23 18:25:27 +00:00
|
|
|
continue
|
|
|
|
return document
|
|
|
|
|
2015-01-09 00:27:28 +00:00
|
|
|
def run_cli(self):
|
|
|
|
logger = self.logger
|
|
|
|
original_less_opts = self.adjust_less_opts()
|
|
|
|
|
2015-01-24 03:18:40 +00:00
|
|
|
self.refresh_completions()
|
2015-05-31 13:59:20 +00:00
|
|
|
|
2015-10-28 13:23:23 +00:00
|
|
|
self.cli = self._build_cli()
|
2015-05-31 13:59:20 +00:00
|
|
|
|
2015-03-01 05:37:59 +00:00
|
|
|
print('Version:', __version__)
|
2015-04-19 05:54:58 +00:00
|
|
|
print('Chat: https://gitter.im/dbcli/pgcli')
|
2015-03-01 06:05:25 +00:00
|
|
|
print('Mail: https://groups.google.com/forum/#!forum/pgcli')
|
|
|
|
print('Home: http://pgcli.com')
|
2015-01-24 03:18:40 +00:00
|
|
|
|
2015-01-09 00:27:28 +00:00
|
|
|
try:
|
|
|
|
while True:
|
2015-09-01 05:07:32 +00:00
|
|
|
document = self.cli.run()
|
2015-01-09 00:27:28 +00:00
|
|
|
|
|
|
|
# The reason we check here instead of inside the pgexecute is
|
|
|
|
# because we want to raise the Exit exception which will be
|
|
|
|
# caught by the try/except block that wraps the pgexecute.run()
|
|
|
|
# statement.
|
|
|
|
if quit_command(document.text):
|
2015-05-02 15:01:57 +00:00
|
|
|
raise EOFError
|
2015-01-10 22:52:50 +00:00
|
|
|
|
2015-04-23 07:26:48 +00:00
|
|
|
try:
|
2015-09-01 05:07:32 +00:00
|
|
|
document = self.handle_editor_command(self.cli, document)
|
2015-04-23 07:26:48 +00:00
|
|
|
except RuntimeError as e:
|
|
|
|
logger.error("sql: %r, error: %r", document.text, e)
|
|
|
|
logger.error("traceback: %r", traceback.format_exc())
|
|
|
|
click.secho(str(e), err=True, fg='red')
|
|
|
|
continue
|
2015-04-18 17:07:06 +00:00
|
|
|
|
2015-10-28 13:23:23 +00:00
|
|
|
# Initialize default metaquery in case execution fails
|
|
|
|
query = MetaQuery(query=document.text, successful=False)
|
2015-01-10 22:52:50 +00:00
|
|
|
|
2015-01-09 00:27:28 +00:00
|
|
|
try:
|
2015-10-28 13:23:23 +00:00
|
|
|
output, query = self._evaluate_command(document.text)
|
2015-01-09 00:27:28 +00:00
|
|
|
except KeyboardInterrupt:
|
|
|
|
# Restart connection to the database
|
2015-10-28 13:23:23 +00:00
|
|
|
self.pgexecute.connect()
|
2015-01-09 00:27:28 +00:00
|
|
|
logger.debug("cancelled query, sql: %r", document.text)
|
|
|
|
click.secho("cancelled query", err=True, fg='red')
|
2015-04-13 22:56:17 +00:00
|
|
|
except NotImplementedError:
|
2015-04-14 02:51:24 +00:00
|
|
|
click.secho('Not Yet Implemented.', fg="yellow")
|
2015-04-18 01:44:29 +00:00
|
|
|
except OperationalError as e:
|
2015-10-28 13:23:23 +00:00
|
|
|
if ('server closed the connection'
|
|
|
|
in utf8tounicode(e.args[0])):
|
|
|
|
self._handle_server_closed_connection()
|
2015-04-18 01:44:29 +00:00
|
|
|
else:
|
|
|
|
logger.error("sql: %r, error: %r", document.text, e)
|
|
|
|
logger.error("traceback: %r", traceback.format_exc())
|
|
|
|
click.secho(str(e), err=True, fg='red')
|
2015-01-09 00:27:28 +00:00
|
|
|
except Exception as e:
|
|
|
|
logger.error("sql: %r, error: %r", document.text, e)
|
|
|
|
logger.error("traceback: %r", traceback.format_exc())
|
|
|
|
click.secho(str(e), err=True, fg='red')
|
2015-01-16 07:41:43 +00:00
|
|
|
else:
|
2015-08-02 01:00:15 +00:00
|
|
|
try:
|
|
|
|
click.echo_via_pager('\n'.join(output))
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
pass
|
2015-01-09 00:27:28 +00:00
|
|
|
|
2015-10-28 13:23:23 +00:00
|
|
|
if self.pgspecial.timing_enabled:
|
|
|
|
print('Time: %0.03fs' % query.total_time)
|
|
|
|
|
|
|
|
# Check if we need to update completions, in order of most
|
|
|
|
# to least drastic changes
|
|
|
|
if query.db_changed:
|
|
|
|
self.refresh_completions(reset=True)
|
|
|
|
elif query.meta_changed:
|
|
|
|
self.refresh_completions(reset=False)
|
|
|
|
elif query.path_changed:
|
2015-10-20 10:51:39 +00:00
|
|
|
logger.debug('Refreshing search path')
|
|
|
|
with self._completer_lock:
|
2015-10-28 13:23:23 +00:00
|
|
|
self.completer.set_search_path(
|
|
|
|
self.pgexecute.search_path())
|
|
|
|
logger.debug('Search path: %r',
|
|
|
|
self.completer.search_path)
|
2015-01-31 21:46:32 +00:00
|
|
|
|
2015-01-10 22:52:50 +00:00
|
|
|
self.query_history.append(query)
|
2015-01-09 00:44:24 +00:00
|
|
|
|
2015-05-02 15:01:57 +00:00
|
|
|
except EOFError:
|
2015-04-07 17:42:25 +00:00
|
|
|
print ('Goodbye!')
|
2015-01-09 00:27:28 +00:00
|
|
|
finally: # Reset the less opts back to original.
|
|
|
|
logger.debug('Restoring env var LESS to %r.', original_less_opts)
|
|
|
|
os.environ['LESS'] = original_less_opts
|
|
|
|
|
2015-10-28 13:23:23 +00:00
|
|
|
def _build_cli(self):
|
|
|
|
|
|
|
|
def set_vi_mode(value):
|
|
|
|
self.vi_mode = value
|
|
|
|
|
|
|
|
key_binding_manager = pgcli_bindings(
|
|
|
|
get_vi_mode_enabled=lambda: self.vi_mode,
|
|
|
|
set_vi_mode_enabled=set_vi_mode)
|
|
|
|
|
|
|
|
def prompt_tokens(_):
|
|
|
|
return [(Token.Prompt, '%s> ' % self.pgexecute.dbname)]
|
|
|
|
|
|
|
|
get_toolbar_tokens = create_toolbar_tokens_func(
|
|
|
|
lambda: self.vi_mode, self.completion_refresher.is_refreshing)
|
|
|
|
|
|
|
|
layout = create_default_layout(
|
|
|
|
lexer=PostgresLexer,
|
|
|
|
reserve_space_for_menu=True,
|
|
|
|
get_prompt_tokens=prompt_tokens,
|
|
|
|
get_bottom_toolbar_tokens=get_toolbar_tokens,
|
|
|
|
display_completions_in_columns=self.wider_completion_menu,
|
|
|
|
multiline=True,
|
|
|
|
extra_input_processors=[
|
|
|
|
# Highlight matching brackets while editing.
|
|
|
|
ConditionalProcessor(
|
|
|
|
processor=HighlightMatchingBracketProcessor(chars='[](){}'),
|
|
|
|
filter=HasFocus(DEFAULT_BUFFER) & ~IsDone()),
|
|
|
|
])
|
|
|
|
|
|
|
|
history_file = self.config['main']['history_file']
|
2015-11-05 02:21:59 +00:00
|
|
|
if history_file == 'default':
|
|
|
|
history_file = config_location() + 'history'
|
2015-10-28 13:23:23 +00:00
|
|
|
with self._completer_lock:
|
|
|
|
buf = PGBuffer(
|
|
|
|
always_multiline=self.multi_line,
|
|
|
|
completer=self.completer,
|
|
|
|
history=FileHistory(os.path.expanduser(history_file)),
|
|
|
|
complete_while_typing=Always())
|
|
|
|
|
|
|
|
application = Application(
|
|
|
|
style=style_factory(self.syntax_style, self.cli_style),
|
|
|
|
layout=layout,
|
|
|
|
buffer=buf,
|
|
|
|
key_bindings_registry=key_binding_manager.registry,
|
|
|
|
on_exit=AbortAction.RAISE_EXCEPTION,
|
|
|
|
ignore_case=True)
|
|
|
|
|
|
|
|
cli = CommandLineInterface(
|
|
|
|
application=application,
|
|
|
|
eventloop=create_eventloop())
|
|
|
|
|
|
|
|
return cli
|
|
|
|
|
|
|
|
def _evaluate_command(self, text):
|
|
|
|
"""Used to run a command entered by the user during CLI operation
|
|
|
|
(Puts the E in REPL)
|
|
|
|
|
|
|
|
returns (results, MetaQuery)
|
|
|
|
"""
|
|
|
|
logger = self.logger
|
|
|
|
logger.debug('sql: %r', text)
|
|
|
|
|
|
|
|
all_success = True
|
|
|
|
meta_changed = False # CREATE, ALTER, DROP, etc
|
|
|
|
mutated = False # INSERT, DELETE, etc
|
|
|
|
db_changed = False
|
|
|
|
path_changed = False
|
|
|
|
output = []
|
|
|
|
total = 0
|
|
|
|
|
|
|
|
# Run the query.
|
|
|
|
start = time()
|
|
|
|
on_error_resume = self.on_error == 'RESUME'
|
|
|
|
res = self.pgexecute.run(text, self.pgspecial,
|
|
|
|
exception_formatter, on_error_resume)
|
|
|
|
|
|
|
|
for title, cur, headers, status, sql, success in res:
|
|
|
|
logger.debug("headers: %r", headers)
|
|
|
|
logger.debug("rows: %r", cur)
|
|
|
|
logger.debug("status: %r", status)
|
|
|
|
threshold = 1000
|
|
|
|
if (is_select(status) and
|
|
|
|
cur and cur.rowcount > threshold):
|
|
|
|
click.secho('The result set has more than %s rows.'
|
|
|
|
% threshold, fg='red')
|
|
|
|
if not click.confirm('Do you want to continue?'):
|
|
|
|
click.secho("Aborted!", err=True, fg='red')
|
|
|
|
break
|
|
|
|
|
|
|
|
if self.pgspecial.auto_expand:
|
|
|
|
max_width = self.cli.output.get_size().columns
|
|
|
|
else:
|
|
|
|
max_width = None
|
|
|
|
|
|
|
|
formatted = format_output(
|
|
|
|
title, cur, headers, status, self.table_format,
|
|
|
|
self.pgspecial.expanded_output, max_width)
|
|
|
|
|
|
|
|
output.extend(formatted)
|
|
|
|
end = time()
|
|
|
|
total += end - start
|
|
|
|
|
|
|
|
# Keep track of whether any of the queries are mutating or changing
|
|
|
|
# the database
|
|
|
|
if success:
|
|
|
|
mutated = mutated or is_mutating(status)
|
|
|
|
db_changed = db_changed or has_change_db_cmd(sql)
|
|
|
|
meta_changed = meta_changed or has_meta_cmd(sql)
|
|
|
|
path_changed = path_changed or has_change_path_cmd(sql)
|
|
|
|
else:
|
|
|
|
all_success = False
|
|
|
|
|
|
|
|
meta_query = MetaQuery(text, all_success, total, meta_changed,
|
|
|
|
db_changed, path_changed, mutated)
|
|
|
|
|
|
|
|
return output, meta_query
|
|
|
|
|
|
|
|
def _handle_server_closed_connection(self):
|
|
|
|
"""Used during CLI execution"""
|
|
|
|
reconnect = click.prompt(
|
|
|
|
'Connection reset. Reconnect (Y/n)',
|
|
|
|
show_default=False, type=bool, default=True)
|
|
|
|
if reconnect:
|
|
|
|
try:
|
|
|
|
self.pgexecute.connect()
|
|
|
|
click.secho('Reconnected!\nTry the command again.', fg='green')
|
|
|
|
except OperationalError as e:
|
|
|
|
click.secho(str(e), err=True, fg='red')
|
|
|
|
|
2015-01-09 00:27:28 +00:00
|
|
|
def adjust_less_opts(self):
|
|
|
|
less_opts = os.environ.get('LESS', '')
|
|
|
|
self.logger.debug('Original value for LESS env var: %r', less_opts)
|
2015-08-12 03:54:07 +00:00
|
|
|
os.environ['LESS'] = '-SRXF'
|
2015-01-09 00:27:28 +00:00
|
|
|
|
|
|
|
return less_opts
|
|
|
|
|
2015-10-18 14:50:08 +00:00
|
|
|
def refresh_completions(self, reset=False):
|
|
|
|
if reset:
|
|
|
|
with self._completer_lock:
|
|
|
|
self.completer.reset_completions()
|
2015-09-04 05:37:31 +00:00
|
|
|
self.completion_refresher.refresh(self.pgexecute, self.pgspecial,
|
2015-09-07 12:40:32 +00:00
|
|
|
self._on_completions_refreshed)
|
2015-09-04 05:37:31 +00:00
|
|
|
return [(None, None, None,
|
|
|
|
'Auto-completion refresh started in the background.')]
|
2015-09-01 07:22:40 +00:00
|
|
|
|
2015-09-07 12:40:32 +00:00
|
|
|
def _on_completions_refreshed(self, new_completer):
|
|
|
|
self._swap_completer_objects(new_completer)
|
|
|
|
|
|
|
|
if self.cli:
|
|
|
|
# After refreshing, redraw the CLI to clear the statusbar
|
|
|
|
# "Refreshing completions..." indicator
|
|
|
|
self.cli.request_redraw()
|
|
|
|
|
2015-09-04 05:53:06 +00:00
|
|
|
def _swap_completer_objects(self, new_completer):
|
2015-09-04 05:37:31 +00:00
|
|
|
"""Swap the completer object in cli with the newly created completer.
|
|
|
|
"""
|
|
|
|
with self._completer_lock:
|
2015-09-04 05:53:06 +00:00
|
|
|
self.completer = new_completer
|
2015-09-04 05:37:31 +00:00
|
|
|
# When pgcli is first launched we call refresh_completions before
|
|
|
|
# instantiating the cli object. So it is necessary to check if cli
|
|
|
|
# exists before trying the replace the completer object in cli.
|
2015-09-05 15:05:30 +00:00
|
|
|
if self.cli:
|
2015-09-04 05:53:06 +00:00
|
|
|
self.cli.current_buffer.completer = new_completer
|
2015-09-01 05:07:32 +00:00
|
|
|
|
2015-01-24 03:19:07 +00:00
|
|
|
def get_completions(self, text, cursor_positition):
|
2015-09-01 05:07:32 +00:00
|
|
|
with self._completer_lock:
|
|
|
|
return self.completer.get_completions(
|
|
|
|
Document(text=text, cursor_position=cursor_positition), None)
|
2015-01-24 03:19:07 +00:00
|
|
|
|
2015-06-25 10:27:29 +00:00
|
|
|
|
2015-01-09 00:27:28 +00:00
|
|
|
@click.command()
|
2015-01-08 12:58:44 +00:00
|
|
|
# Default host is '' so psycopg2 can default to either localhost or unix socket
|
|
|
|
@click.option('-h', '--host', default='', envvar='PGHOST',
|
2015-01-06 20:06:27 +00:00
|
|
|
help='Host address of the postgres database.')
|
2014-12-11 18:10:26 +00:00
|
|
|
@click.option('-p', '--port', default=5432, help='Port number at which the '
|
2015-01-06 20:06:27 +00:00
|
|
|
'postgres instance is listening.', envvar='PGPORT')
|
2015-01-07 21:50:48 +00:00
|
|
|
@click.option('-U', '--user', envvar='PGUSER', help='User name to '
|
2014-12-11 18:10:26 +00:00
|
|
|
'connect to the postgres database.')
|
2015-01-07 21:50:48 +00:00
|
|
|
@click.option('-W', '--password', 'prompt_passwd', is_flag=True, default=False,
|
2015-01-14 05:53:53 +00:00
|
|
|
help='Force password prompt.')
|
2015-01-07 21:50:48 +00:00
|
|
|
@click.option('-w', '--no-password', 'never_prompt', is_flag=True,
|
2015-01-14 05:53:53 +00:00
|
|
|
default=False, help='Never prompt for password.')
|
2015-02-14 01:02:24 +00:00
|
|
|
@click.option('-v', '--version', is_flag=True, help='Version of pgcli.')
|
2015-01-14 05:53:53 +00:00
|
|
|
@click.option('-d', '--dbname', default='', envvar='PGDATABASE',
|
|
|
|
help='database name to connect to.')
|
2015-11-05 02:21:59 +00:00
|
|
|
@click.option('--pgclirc', default=config_location() + 'config',
|
|
|
|
envvar='PGCLIRC', help='Location of pgclirc file.')
|
2015-01-14 18:07:35 +00:00
|
|
|
@click.argument('database', default=lambda: None, envvar='PGDATABASE', nargs=1)
|
|
|
|
@click.argument('username', default=lambda: None, envvar='PGUSER', nargs=1)
|
2015-01-14 05:53:53 +00:00
|
|
|
def cli(database, user, host, port, prompt_passwd, never_prompt, dbname,
|
2015-08-06 05:36:45 +00:00
|
|
|
username, version, pgclirc):
|
2015-02-14 01:02:24 +00:00
|
|
|
|
|
|
|
if version:
|
|
|
|
print('Version:', __version__)
|
|
|
|
sys.exit(0)
|
|
|
|
|
2015-09-23 05:05:03 +00:00
|
|
|
config_dir = os.path.dirname(config_location())
|
|
|
|
if not os.path.exists(config_dir):
|
|
|
|
os.makedirs(config_dir)
|
2015-09-20 21:57:12 +00:00
|
|
|
|
2015-09-23 05:05:03 +00:00
|
|
|
# Migrate the config file from old location.
|
2015-11-05 16:15:32 +00:00
|
|
|
config_full_path = config_location() + 'config'
|
2015-09-20 21:57:12 +00:00
|
|
|
if os.path.exists(os.path.expanduser('~/.pgclirc')):
|
2015-11-05 16:15:32 +00:00
|
|
|
if not os.path.exists(config_full_path):
|
|
|
|
shutil.move(os.path.expanduser('~/.pgclirc'), config_full_path)
|
2015-09-20 21:57:12 +00:00
|
|
|
print ('Config file (~/.pgclirc) moved to new location',
|
2015-11-05 16:15:32 +00:00
|
|
|
config_full_path)
|
2015-09-20 21:57:12 +00:00
|
|
|
else:
|
2015-11-05 16:15:32 +00:00
|
|
|
print ('Config file is now located at', config_full_path)
|
2015-09-20 21:57:12 +00:00
|
|
|
print ('Please move the existing config file ~/.pgclirc to',
|
2015-11-05 16:15:32 +00:00
|
|
|
config_full_path)
|
2015-09-20 21:57:12 +00:00
|
|
|
|
2015-08-06 05:36:45 +00:00
|
|
|
pgcli = PGCli(prompt_passwd, never_prompt, pgclirc_file=pgclirc)
|
2015-01-09 00:27:28 +00:00
|
|
|
|
2015-01-14 18:07:35 +00:00
|
|
|
# Choose which ever one has a valid value.
|
2015-01-14 05:53:53 +00:00
|
|
|
database = database or dbname
|
2015-01-14 18:07:35 +00:00
|
|
|
user = username or user
|
2015-01-14 05:53:53 +00:00
|
|
|
|
2015-01-09 00:27:28 +00:00
|
|
|
if '://' in database:
|
|
|
|
pgcli.connect_uri(database)
|
2015-08-07 23:32:39 +00:00
|
|
|
elif "=" in database:
|
|
|
|
pgcli.connect_dsn(database)
|
|
|
|
elif os.environ.get('PGSERVICE', None):
|
|
|
|
pgcli.connect_dsn('service={0}'.format(os.environ['PGSERVICE']))
|
2015-01-09 00:27:28 +00:00
|
|
|
else:
|
|
|
|
pgcli.connect(database, host, user, port)
|
|
|
|
|
|
|
|
pgcli.logger.debug('Launch Params: \n'
|
2015-01-04 22:52:04 +00:00
|
|
|
'\tdatabase: %r'
|
|
|
|
'\tuser: %r'
|
|
|
|
'\thost: %r'
|
2015-01-09 00:27:28 +00:00
|
|
|
'\tport: %r', database, user, host, port)
|
2014-10-12 17:31:54 +00:00
|
|
|
|
2015-10-23 09:49:20 +00:00
|
|
|
if setproctitle:
|
|
|
|
obfuscate_process_password()
|
|
|
|
|
2015-01-09 00:27:28 +00:00
|
|
|
pgcli.run_cli()
|
2014-12-05 16:56:59 +00:00
|
|
|
|
2015-10-28 13:23:23 +00:00
|
|
|
|
2015-10-19 17:40:50 +00:00
|
|
|
def obfuscate_process_password():
|
2015-10-18 20:55:56 +00:00
|
|
|
process_title = setproctitle.getproctitle()
|
|
|
|
if '://' in process_title:
|
|
|
|
process_title = re.sub(r":(.*):(.*)@", r":\1:xxxx@", process_title)
|
|
|
|
elif "=" in process_title:
|
2015-10-23 09:49:20 +00:00
|
|
|
process_title = re.sub(r"password=(.+?)((\s[a-zA-Z]+=)|$)", r"password=xxxx\2", process_title)
|
2015-10-18 20:55:56 +00:00
|
|
|
|
|
|
|
setproctitle.setproctitle(process_title)
|
|
|
|
|
2015-09-23 14:26:34 +00:00
|
|
|
def format_output(title, cur, headers, status, table_format, expanded=False, max_width=None):
|
2015-01-07 22:17:55 +00:00
|
|
|
output = []
|
2015-03-25 07:22:20 +00:00
|
|
|
if title: # Only print the title if it's not None.
|
|
|
|
output.append(title)
|
2015-01-18 08:25:36 +00:00
|
|
|
if cur:
|
2015-02-05 08:48:40 +00:00
|
|
|
headers = [utf8tounicode(x) for x in headers]
|
2015-09-29 19:30:04 +00:00
|
|
|
if expanded and headers:
|
2015-01-18 08:25:36 +00:00
|
|
|
output.append(expanded_table(cur, headers))
|
2015-01-09 09:03:09 +00:00
|
|
|
else:
|
2015-09-22 21:21:02 +00:00
|
|
|
tabulated, rows = tabulate(cur, headers, tablefmt=table_format,
|
|
|
|
missingval='<null>')
|
2015-10-19 13:18:41 +00:00
|
|
|
if (max_width and
|
|
|
|
content_exceeds_width(rows[0], max_width) and
|
|
|
|
headers):
|
2015-09-22 21:21:02 +00:00
|
|
|
output.append(expanded_table(rows, headers))
|
|
|
|
else:
|
|
|
|
output.append(tabulated)
|
2015-01-07 22:17:55 +00:00
|
|
|
if status: # Only print the status if it's not None.
|
|
|
|
output.append(status)
|
2015-01-08 09:58:02 +00:00
|
|
|
return output
|
2015-01-07 22:17:55 +00:00
|
|
|
|
2015-10-28 13:23:23 +00:00
|
|
|
|
|
|
|
def has_meta_cmd(query):
|
2015-01-31 21:46:32 +00:00
|
|
|
"""Determines if the completion needs a refresh by checking if the sql
|
2015-10-28 13:23:23 +00:00
|
|
|
statement is an alter, create, or drop"""
|
|
|
|
try:
|
|
|
|
first_token = query.split()[0]
|
|
|
|
if first_token.lower() in ('alter', 'create', 'drop'):
|
|
|
|
return True
|
|
|
|
except Exception:
|
|
|
|
return False
|
2015-01-31 21:46:32 +00:00
|
|
|
|
2015-10-03 18:41:31 +00:00
|
|
|
return False
|
|
|
|
|
2015-10-18 14:50:08 +00:00
|
|
|
|
2015-10-28 13:23:23 +00:00
|
|
|
def has_change_db_cmd(query):
|
|
|
|
"""Determines if the statement is a database switch such as 'use' or '\\c'"""
|
|
|
|
try:
|
|
|
|
first_token = query.split()[0]
|
|
|
|
if first_token.lower() in ('use', '\\c', '\\connect'):
|
|
|
|
return True
|
|
|
|
except Exception:
|
|
|
|
return False
|
2015-10-18 14:50:08 +00:00
|
|
|
|
2015-10-28 13:23:23 +00:00
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def has_change_path_cmd(sql):
|
2015-01-31 21:46:32 +00:00
|
|
|
"""Determines if the search_path should be refreshed by checking if the
|
|
|
|
sql has 'set search_path'."""
|
|
|
|
return 'set search_path' in sql.lower()
|
2015-01-26 13:05:39 +00:00
|
|
|
|
2015-10-28 13:23:23 +00:00
|
|
|
|
2015-01-10 22:52:50 +00:00
|
|
|
def is_mutating(status):
|
2015-01-18 08:25:36 +00:00
|
|
|
"""Determines if the statement is mutating based on the status."""
|
2015-01-14 12:51:16 +00:00
|
|
|
if not status:
|
|
|
|
return False
|
2015-01-16 07:41:43 +00:00
|
|
|
|
2015-10-28 13:23:23 +00:00
|
|
|
mutating = set(['insert', 'update', 'delete'])
|
2015-01-10 22:52:50 +00:00
|
|
|
return status.split(None, 1)[0].lower() in mutating
|
|
|
|
|
2015-10-28 13:23:23 +00:00
|
|
|
|
2015-01-18 08:25:36 +00:00
|
|
|
def is_select(status):
|
|
|
|
"""Returns true if the first word in status is 'select'."""
|
|
|
|
if not status:
|
|
|
|
return False
|
|
|
|
return status.split(None, 1)[0].lower() == 'select'
|
|
|
|
|
2015-10-28 13:23:23 +00:00
|
|
|
|
2015-01-04 22:52:04 +00:00
|
|
|
def quit_command(sql):
|
|
|
|
return (sql.strip().lower() == 'exit'
|
|
|
|
or sql.strip().lower() == 'quit'
|
|
|
|
or sql.strip() == '\q'
|
|
|
|
or sql.strip() == ':q')
|
|
|
|
|
2015-10-21 20:56:11 +00:00
|
|
|
|
|
|
|
def exception_formatter(e):
|
2015-10-24 17:43:28 +00:00
|
|
|
return click.style(utf8tounicode(str(e)), fg='red')
|
2015-10-21 20:56:11 +00:00
|
|
|
|
|
|
|
|
2015-01-06 19:49:29 +00:00
|
|
|
if __name__ == "__main__":
|
|
|
|
cli()
|