summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--.github/workflows/ci.yml56
-rw-r--r--.travis.yml33
-rw-r--r--README.md8
-rw-r--r--changelog.md55
-rw-r--r--mycli/AUTHORS10
-rw-r--r--mycli/__init__.py2
-rw-r--r--mycli/clibuffer.py2
-rw-r--r--mycli/clistyle.py34
-rw-r--r--mycli/clitoolbar.py1
-rw-r--r--mycli/magic.py2
-rwxr-xr-xmycli/main.py90
-rw-r--r--mycli/myclirc32
-rw-r--r--mycli/packages/completion_engine.py3
-rw-r--r--mycli/packages/parseutils.py4
-rw-r--r--mycli/packages/special/iocommands.py118
-rw-r--r--mycli/packages/special/main.py2
-rw-r--r--mycli/sqlcompleter.py2
-rw-r--r--mycli/sqlexecute.py21
-rw-r--r--requirements-dev.txt4
-rwxr-xr-xsetup.py8
-rw-r--r--test/conftest.py2
-rw-r--r--test/features/crud_database.feature4
-rw-r--r--test/features/db_utils.py18
-rw-r--r--test/features/environment.py18
-rw-r--r--test/features/fixture_data/help_commands.txt2
-rw-r--r--test/features/steps/crud_database.py19
-rw-r--r--test/features/steps/wrappers.py2
-rw-r--r--test/test_main.py34
-rw-r--r--test/test_special_iocommands.py25
-rw-r--r--test/test_sqlexecute.py2
-rw-r--r--test/test_tabular_output.py2
31 files changed, 495 insertions, 120 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..413b749
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,56 @@
+name: mycli
+
+on:
+ pull_request:
+ paths-ignore:
+ - '**.md'
+
+jobs:
+ linux:
+
+ runs-on: ubuntu-latest
+
+ strategy:
+ matrix:
+ python-version: [3.6, 3.7, 3.8, 3.9]
+
+ steps:
+
+ - uses: actions/checkout@v2
+
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v2
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: Start MySQL
+ run: |
+ sudo /etc/init.d/mysql start
+
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install -r requirements-dev.txt
+ pip install --no-cache-dir -e .
+
+ - name: Wait for MySQL connection
+ run: |
+ while ! mysqladmin ping --host=localhost --port=3306 --user=root --password=root --silent; do
+ sleep 5
+ done
+
+ - name: Pytest / behave
+ env:
+ PYTEST_PASSWORD: root
+ run: |
+ ./setup.py test --pytest-args="--cov-report= --cov=mycli"
+
+ - name: Lint
+ run: |
+ ./setup.py lint --branch=HEAD
+
+ - name: Coverage
+ run: |
+ coverage combine
+ coverage report
+ codecov
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 0afb5cc..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,33 +0,0 @@
-language: python
-python:
- - "3.6"
- - "3.7"
- - "3.8"
-
-matrix:
- include:
- - python: 3.7
- dist: xenial
- sudo: true
-
-install:
- - pip install -r requirements-dev.txt
- - pip install -e .
- - sudo rm -f /etc/mysql/conf.d/performance-schema.cnf
- - sudo service mysql restart
-
-script:
- - ./setup.py test --pytest-args="--cov-report= --cov=mycli"
- - coverage combine
- - coverage report
- - ./setup.py lint --branch=$TRAVIS_BRANCH
-
-after_success:
- - codecov
-
-notifications:
- webhooks:
- urls:
- - YOUR_WEBHOOK_URL
- on_success: change # options: [always|never|change] default: always
- on_failure: always # options: [always|never|change] default: always
diff --git a/README.md b/README.md
index efe804d..c709eb8 100644
--- a/README.md
+++ b/README.md
@@ -63,8 +63,8 @@ $ sudo apt-get install mycli # Only on debian or ubuntu
--ssh-password TEXT Password to connect to ssh server.
--ssh-key-filename TEXT Private key filename (identify file) for the
ssh connection.
- --ssh-config-path TEXT Path to ssh configuation.
- --ssh-config-host TEXT Host for ssh server in ssh configuations (requires paramiko).
+ --ssh-config-path TEXT Path to ssh configuration.
+ --ssh-config-host TEXT Host for ssh server in ssh configurations (requires paramiko).
--ssl-ca PATH CA file in PEM format.
--ssl-capath TEXT CA directory.
--ssl-cert PATH X509 cert in PEM format.
@@ -96,6 +96,8 @@ $ sudo apt-get install mycli # Only on debian or ubuntu
--local-infile BOOLEAN Enable/disable LOAD DATA LOCAL INFILE.
--login-path TEXT Read this path from the login file.
-e, --execute TEXT Execute command and quit.
+ --init-command TEXT SQL statement to execute after connecting.
+ --charset TEXT Character set for MySQL session.
--help Show this message and exit.
Features
@@ -112,7 +114,7 @@ Features
* Support for multiline queries.
* Favorite queries with optional positional parameters. Save a query using
`\fs alias query` and execute it with `\f alias` whenever you need.
-* Timing of sql statments and table rendering.
+* Timing of sql statements and table rendering.
* Config file is automatically created at ``~/.myclirc`` at first launch.
* Log every query and its results to a file (disabled by default).
* Pretty prints tabular data (with colors!)
diff --git a/changelog.md b/changelog.md
index a4fea35..fe6e268 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,9 +1,51 @@
+1.23.2
+===
+
+Bug Fixes:
+----------
+* Ensure `--port` is always an int.
+
+1.23.1
+===
+
+Bug Fixes:
+----------
+* Allow `--host` without `--port` to make a TCP connection.
+
+1.23.0
+===
+
+Features:
+---------
+
+* Add an option `--init-command` to execute SQL after connecting (Thanks: [KITAGAWA Yasutaka]).
+* Use InputMode.REPLACE_SINGLE
+* Add support for ANSI escape sequences for coloring the prompt.
+* Allow customization of Pygments SQL syntax-highlighting styles.
+* Add a `\clip` special command to copy queries to the system clipboard.
+* Add a special command `\pipe_once` to pipe output to a subprocess.
+* Add an option `--charset` to set the default charset when connect database.
+
+Bug Fixes:
+----------
+* Fixed compatibility with sqlparse 0.4 (Thanks: [mtorromeo]).
+* Fixed iPython magic (Thanks: [mwcm]).
+* Send "Connecting to socket" message to the standard error.
+* Respect empty string for prompt_continuation via `prompt_continuation = ''` in `.myclirc`
+* Fix \once -o to overwrite output whole, instead of line-by-line.
+* Dispatch lines ending with `\e` or `\clip` on return, even in multiline mode.
+* Restore working local `--socket=<UDS>` (Thanks: [xeron]).
+* Allow backtick quoting around the database argument to the `use` command.
+* Avoid opening `/dev/tty` when `--no-warn` is given.
+* Fixed some typo errors in `README.md`.
+
1.22.2
======
Bug Fixes:
----------
-* Make the `pwd` module optional.
+
+* Make the `pwd` module optional.
1.22.1
======
@@ -18,6 +60,11 @@ Features:
* Add an option `--list-ssh-config` to list ssh configurations.
* Add an option `--ssh-config-path` to choose ssh configuration path.
+Bug Fixes:
+----------
+
+* Fix specifying empty password with `--password=''` when config file has a password set (Thanks: [Zach DeCook]).
+
1.21.1
======
@@ -28,6 +75,7 @@ Bug Fixes:
* Fix broken auto-completion for favorite queries (Thanks: [Amjith]).
* Fix undefined variable exception when running with --no-warn (Thanks: [Georgy Frolov])
+* Support setting color for null value (Thanks: [laixintao])
1.21.0
======
@@ -768,3 +816,8 @@ Bug Fixes:
[François Pietka]: https://github.com/fpietka
[Frederic Aoustin]: https://github.com/fraoustin
[Georgy Frolov]: https://github.com/pasenor
+[Zach DeCook]: https://zachdecook.com
+[laixintao]: https://github.com/laixintao
+[mtorromeo]: https://github.com/mtorromeo
+[mwcm]: https://github.com/mwcm
+[xeron]: https://github.com/xeron
diff --git a/mycli/AUTHORS b/mycli/AUTHORS
index b3636d9..221ce8b 100644
--- a/mycli/AUTHORS
+++ b/mycli/AUTHORS
@@ -72,6 +72,16 @@ Contributors:
* Jakub Boukal
* Takeshi D. Itoh
* laixintao
+ * Zach DeCook
+ * kevinhwang91
+ * KITAGAWA Yasutaka
+ * bitkeen
+ * Morgan Mitchell
+ * Massimiliano Torromeo
+ * Roland Walker
+ * xeron
+ * 0xflotus
+ * Seamile
Creator:
--------
diff --git a/mycli/__init__.py b/mycli/__init__.py
index 53bfe2e..375471f 100644
--- a/mycli/__init__.py
+++ b/mycli/__init__.py
@@ -1 +1 @@
-__version__ = '1.22.2'
+__version__ = '1.23.2'
diff --git a/mycli/clibuffer.py b/mycli/clibuffer.py
index c9d29d1..c0cb5c1 100644
--- a/mycli/clibuffer.py
+++ b/mycli/clibuffer.py
@@ -39,6 +39,8 @@ def _multiline_exception(text):
text.endswith('\\g') or
text.endswith('\\G') or
+ text.endswith(r'\e') or
+ text.endswith(r'\clip') or
# Exit doesn't need semi-column`
(text == 'exit') or
diff --git a/mycli/clistyle.py b/mycli/clistyle.py
index c94f793..b0ac992 100644
--- a/mycli/clistyle.py
+++ b/mycli/clistyle.py
@@ -34,6 +34,7 @@ TOKEN_TO_PROMPT_STYLE = {
Token.Output.Header: 'output.header',
Token.Output.OddRow: 'output.odd-row',
Token.Output.EvenRow: 'output.even-row',
+ Token.Output.Null: 'output.null',
Token.Prompt: 'prompt',
Token.Continuation: 'continuation',
}
@@ -43,6 +44,36 @@ PROMPT_STYLE_TO_TOKEN = {
v: k for k, v in TOKEN_TO_PROMPT_STYLE.items()
}
+# all tokens that the Pygments MySQL lexer can produce
+OVERRIDE_STYLE_TO_TOKEN = {
+ 'sql.comment': Token.Comment,
+ 'sql.comment.multi-line': Token.Comment.Multiline,
+ 'sql.comment.single-line': Token.Comment.Single,
+ 'sql.comment.optimizer-hint': Token.Comment.Special,
+ 'sql.escape': Token.Error,
+ 'sql.keyword': Token.Keyword,
+ 'sql.datatype': Token.Keyword.Type,
+ 'sql.literal': Token.Literal,
+ 'sql.literal.date': Token.Literal.Date,
+ 'sql.symbol': Token.Name,
+ 'sql.quoted-schema-object': Token.Name.Quoted,
+ 'sql.quoted-schema-object.escape': Token.Name.Quoted.Escape,
+ 'sql.constant': Token.Name.Constant,
+ 'sql.function': Token.Name.Function,
+ 'sql.variable': Token.Name.Variable,
+ 'sql.number': Token.Number,
+ 'sql.number.binary': Token.Number.Bin,
+ 'sql.number.float': Token.Number.Float,
+ 'sql.number.hex': Token.Number.Hex,
+ 'sql.number.integer': Token.Number.Integer,
+ 'sql.operator': Token.Operator,
+ 'sql.punctuation': Token.Punctuation,
+ 'sql.string': Token.String,
+ 'sql.string.double-quouted': Token.String.Double,
+ 'sql.string.escape': Token.String.Escape,
+ 'sql.string.single-quoted': Token.String.Single,
+ 'sql.whitespace': Token.Text,
+}
def parse_pygments_style(token_name, style_object, style_dict):
"""Parse token type and style string.
@@ -107,6 +138,9 @@ def style_factory_output(name, cli_style):
elif token in PROMPT_STYLE_TO_TOKEN:
token_type = PROMPT_STYLE_TO_TOKEN[token]
style.update({token_type: cli_style[token]})
+ elif token in OVERRIDE_STYLE_TO_TOKEN:
+ token_type = OVERRIDE_STYLE_TO_TOKEN[token]
+ style.update({token_type: cli_style[token]})
else:
# TODO: cli helpers will have to switch to ptk.Style
logger.error('Unhandled style / class name: %s', token)
diff --git a/mycli/clitoolbar.py b/mycli/clitoolbar.py
index e03e182..eec2978 100644
--- a/mycli/clitoolbar.py
+++ b/mycli/clitoolbar.py
@@ -48,5 +48,6 @@ def _get_vi_mode():
InputMode.INSERT: 'I',
InputMode.NAVIGATION: 'N',
InputMode.REPLACE: 'R',
+ InputMode.REPLACE_SINGLE: 'R',
InputMode.INSERT_MULTIPLE: 'M',
}[get_app().vi_state.input_mode]
diff --git a/mycli/magic.py b/mycli/magic.py
index 5527f72..b1a3268 100644
--- a/mycli/magic.py
+++ b/mycli/magic.py
@@ -19,7 +19,7 @@ def load_ipython_extension(ipython):
def mycli_line_magic(line):
_logger.debug('mycli magic called: %r', line)
parsed = sql.parse.parse(line, {})
- conn = sql.connection.Connection.get(parsed['connection'])
+ conn = sql.connection.Connection(parsed['connection'])
try:
# A corresponding mycli object already exists
diff --git a/mycli/main.py b/mycli/main.py
index 03797a0..f2b2fd8 100755
--- a/mycli/main.py
+++ b/mycli/main.py
@@ -21,13 +21,14 @@ from cli_helpers.tabular_output import preprocessors
from cli_helpers.utils import strip_ansi
import click
import sqlparse
-from mycli.packages.parseutils import is_dropping_database
+from mycli.packages.parseutils import is_dropping_database, is_destructive
from prompt_toolkit.completion import DynamicCompleter
from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode
from prompt_toolkit.key_binding.bindings.named_commands import register as prompt_register
from prompt_toolkit.shortcuts import PromptSession, CompleteStyle
from prompt_toolkit.document import Document
from prompt_toolkit.filters import HasFocus, IsDone
+from prompt_toolkit.formatted_text import ANSI
from prompt_toolkit.layout.processors import (HighlightMatchingBracketProcessor,
ConditionalProcessor)
from prompt_toolkit.lexers import PygmentsLexer
@@ -98,7 +99,7 @@ class MyCli(object):
xdg_config_home = "~/.config"
system_config_files = [
'/etc/myclirc',
- os.path.join(xdg_config_home, "mycli", "myclirc")
+ os.path.join(os.path.expanduser(xdg_config_home), "mycli", "myclirc")
]
default_config_file = os.path.join(PACKAGE_ROOT, 'myclirc')
@@ -152,7 +153,7 @@ class MyCli(object):
c['main'].as_bool('auto_vertical_output')
# Write user config if system config wasn't the last config loaded.
- if c.filename not in self.system_config_files:
+ if c.filename not in self.system_config_files and not os.path.exists(myclirc):
write_default_config(self.default_config_file, myclirc)
# audit log
@@ -238,6 +239,9 @@ class MyCli(object):
)
return
+ if arg.startswith('`') and arg.endswith('`'):
+ arg = re.sub(r'^`(.*)`$', r'\1', arg)
+ arg = re.sub(r'``', r'`', arg)
self.sqlexecute.change_db(arg)
yield (None, None, None, 'You are now connected to database "%s" as '
@@ -363,7 +367,7 @@ class MyCli(object):
def connect(self, database='', user='', passwd='', host='', port='',
socket='', charset='', local_infile='', ssl='',
ssh_user='', ssh_host='', ssh_port='',
- ssh_password='', ssh_key_filename=''):
+ ssh_password='', ssh_key_filename='', init_command=''):
cnf = {'database': None,
'user': None,
@@ -387,16 +391,16 @@ class MyCli(object):
database = database or cnf['database']
# Socket interface not supported for SSH connections
- if port or host or ssh_host or ssh_port:
+ if port or (host and host != 'localhost') or (ssh_host and ssh_port):
socket = ''
else:
socket = socket or cnf['socket'] or guess_socket_location()
user = user or cnf['user'] or os.getenv('USER')
host = host or cnf['host']
- port = port or cnf['port']
+ port = int(port or cnf['port'] or 3306)
ssl = ssl or {}
- passwd = passwd or cnf['password']
+ passwd = passwd if isinstance(passwd, str) else cnf['password']
charset = charset or cnf['default-character-set'] or 'utf8'
# Favor whichever local_infile option is set.
@@ -420,7 +424,7 @@ class MyCli(object):
self.sqlexecute = SQLExecute(
database, user, passwd, host, port, socket, charset,
local_infile, ssl, ssh_user, ssh_host, ssh_port,
- ssh_password, ssh_key_filename
+ ssh_password, ssh_key_filename, init_command
)
except OperationalError as e:
if ('Access denied for user' in e.args[1]):
@@ -429,7 +433,7 @@ class MyCli(object):
self.sqlexecute = SQLExecute(
database, user, new_passwd, host, port, socket,
charset, local_infile, ssl, ssh_user, ssh_host,
- ssh_port, ssh_password, ssh_key_filename
+ ssh_port, ssh_password, ssh_key_filename, init_command
)
else:
raise e
@@ -438,7 +442,7 @@ class MyCli(object):
if not WIN and socket:
socket_owner = getpwuid(os.stat(socket).st_uid).pw_name
self.echo(
- f"Connecting to socket {socket}, owned by user {socket_owner}")
+ f"Connecting to socket {socket}, owned by user {socket_owner}", err=True)
try:
_connect()
except OperationalError as e:
@@ -481,7 +485,7 @@ class MyCli(object):
exit(1)
def handle_editor_command(self, text):
- """Editor command is any query that is prefixed or suffixed by a '\e'.
+ r"""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:
@@ -511,6 +515,24 @@ class MyCli(object):
continue
return text
+ def handle_clip_command(self, text):
+ r"""A clip command is any query that is prefixed or suffixed by a
+ '\clip'.
+
+ :param text: Document
+ :return: Boolean
+
+ """
+
+ if special.clip_command(text):
+ query = (special.get_clip_query(text) or
+ self.get_last_query())
+ message = special.copy_query_to_clipboard(sql=query)
+ if message:
+ raise RuntimeError(message)
+ return True
+ return False
+
def run_cli(self):
iterations = 0
sqlexecute = self.sqlexecute
@@ -548,10 +570,13 @@ class MyCli(object):
prompt = self.get_prompt(self.prompt_format)
if self.prompt_format == self.default_prompt and len(prompt) > self.max_len_prompt:
prompt = self.get_prompt('\\d> ')
- return [('class:prompt', prompt)]
+ prompt = prompt.replace("\\x1b", "\x1b")
+ return ANSI(prompt)
def get_continuation(width, *_):
- if self.multiline_continuation_char:
+ if self.multiline_continuation_char == '':
+ continuation = ''
+ elif self.multiline_continuation_char:
left_padding = width - len(self.multiline_continuation_char)
continuation = " " * \
max((left_padding - 1), 0) + \
@@ -580,6 +605,15 @@ class MyCli(object):
self.echo(str(e), err=True, fg='red')
return
+ try:
+ if self.handle_clip_command(text):
+ return
+ except RuntimeError as e:
+ logger.error("sql: %r, error: %r", text, e)
+ logger.error("traceback: %r", traceback.format_exc())
+ self.echo(str(e), err=True, fg='red')
+ return
+
if not text.strip():
return
@@ -654,6 +688,7 @@ class MyCli(object):
result_count += 1
mutating = mutating or destroy or is_mutating(status)
special.unset_once_if_written()
+ special.unset_pipe_once_if_written()
except EOFError as e:
raise e
except KeyboardInterrupt:
@@ -814,6 +849,7 @@ class MyCli(object):
self.log_output(line)
special.write_tee(line)
special.write_once(line)
+ special.write_pipe_once(line)
if fits or output_via_pager:
# buffering
@@ -1051,6 +1087,10 @@ class MyCli(object):
help='Read this path from the login file.')
@click.option('-e', '--execute', type=str,
help='Execute command and quit.')
+@click.option('--init-command', type=str,
+ help='SQL statement to execute after connecting.')
+@click.option('--charset', type=str,
+ help='Character set for MySQL session.')
@click.argument('database', default='', nargs=1)
def cli(database, user, host, port, socket, password, dbname,
version, verbose, prompt, logfile, defaults_group_suffix,
@@ -1058,7 +1098,8 @@ def cli(database, user, host, port, socket, password, dbname,
ssl_ca, ssl_capath, ssl_cert, ssl_key, ssl_cipher,
ssl_verify_server_cert, table, csv, warn, execute, myclirc, dsn,
list_dsn, ssh_user, ssh_host, ssh_port, ssh_password,
- ssh_key_filename, list_ssh_config, ssh_config_path, ssh_config_host):
+ ssh_key_filename, list_ssh_config, ssh_config_path, ssh_config_host,
+ init_command, charset):
"""A MySQL terminal client with auto-completion and syntax highlighting.
\b
@@ -1182,7 +1223,9 @@ def cli(database, user, host, port, socket, password, dbname,
ssh_host=ssh_host,
ssh_port=ssh_port,
ssh_password=ssh_password,
- ssh_key_filename=ssh_key_filename
+ ssh_key_filename=ssh_key_filename,
+ init_command=init_command,
+ charset=charset
)
mycli.logger.debug('Launch Params: \n'
@@ -1217,14 +1260,15 @@ def cli(database, user, host, port, socket, password, dbname,
click.secho('Sorry... :(', err=True, fg='red')
exit(1)
- try:
- sys.stdin = open('/dev/tty')
- except (IOError, OSError):
- mycli.logger.warning('Unable to open TTY as stdin.')
+ if mycli.destructive_warning and is_destructive(stdin_text):
+ try:
+ sys.stdin = open('/dev/tty')
+ warn_confirmed = confirm_destructive_query(stdin_text)
+ except (IOError, OSError):
+ mycli.logger.warning('Unable to open TTY as stdin.')
+ if not warn_confirmed:
+ exit(0)
- if (mycli.destructive_warning and
- confirm_destructive_query(stdin_text) is False):
- exit(0)
try:
new_line = True
@@ -1287,7 +1331,7 @@ def is_select(status):
def thanks_picker(files=()):
contents = []
for line in fileinput.input(files=files):
- m = re.match('^ *\* (.*)', line)
+ m = re.match(r'^ *\* (.*)', line)
if m:
contents.append(m.group(1))
return choice(contents)
diff --git a/mycli/myclirc b/mycli/myclirc
index 534b201..0bde200 100644
--- a/mycli/myclirc
+++ b/mycli/myclirc
@@ -41,6 +41,7 @@ table_format = ascii
# friendly, monokai, paraiso, colorful, murphy, bw, pastie, paraiso, trac, default,
# fruity.
# Screenshots at http://mycli.net/syntax
+# Can be further modified in [colors]
syntax_style = default
# Keybindings: Possible values: emacs, vi.
@@ -65,6 +66,7 @@ wider_completion_menu = False
# \t - Product type (Percona, MySQL, MariaDB)
# \A - DSN alias name (from the [alias_dsn] section)
# \u - Username
+# \x1b[...m - insert ANSI escape sequence
prompt = '\t \u@\h:\d> '
prompt_continuation = '->'
@@ -111,6 +113,36 @@ bottom-toolbar.transaction.failed = 'bg:#222222 #ff005f bold'
output.header = "#00ff5f bold"
output.odd-row = ""
output.even-row = ""
+output.null = "#808080"
+
+# SQL syntax highlighting overrides
+# sql.comment = 'italic #408080'
+# sql.comment.multi-line = ''
+# sql.comment.single-line = ''
+# sql.comment.optimizer-hint = ''
+# sql.escape = 'border:#FF0000'
+# sql.keyword = 'bold #008000'
+# sql.datatype = 'nobold #B00040'
+# sql.literal = ''
+# sql.literal.date = ''
+# sql.symbol = ''
+# sql.quoted-schema-object = ''
+# sql.quoted-schema-object.escape = ''
+# sql.constant = '#880000'
+# sql.function = '#0000FF'
+# sql.variable = '#19177C'
+# sql.number = '#666666'
+# sql.number.binary = ''
+# sql.number.float = ''
+# sql.number.hex = ''
+# sql.number.integer = ''
+# sql.operator = '#666666'
+# sql.punctuation = ''
+# sql.string = '#BA2121'
+# sql.string.double-quouted = ''
+# sql.string.escape = 'bold #BB6622'
+# sql.string.single-quoted = ''
+# sql.whitespace = ''
# Favorite queries.
[favorite_queries]
diff --git a/mycli/packages/completion_engine.py b/mycli/packages/completion_engine.py
index 2b19c32..3cff2cc 100644
--- a/mycli/packages/completion_engine.py
+++ b/mycli/packages/completion_engine.py
@@ -2,7 +2,6 @@ import os
import sys
import sqlparse
from sqlparse.sql import Comparison, Identifier, Where
-from sqlparse.compat import text_type
from .parseutils import last_word, extract_tables, find_prev_keyword
from .special import parse_special_command
@@ -55,7 +54,7 @@ def suggest_type(full_text, text_before_cursor):
stmt_start, stmt_end = 0, 0
for statement in parsed:
- stmt_len = len(text_type(statement))
+ stmt_len = len(str(statement))
stmt_start, stmt_end = stmt_end, stmt_end + stmt_len
if stmt_end >= current_pos:
diff --git a/mycli/packages/parseutils.py b/mycli/packages/parseutils.py
index e3b383e..268e04e 100644
--- a/mycli/packages/parseutils.py
+++ b/mycli/packages/parseutils.py
@@ -11,11 +11,11 @@ cleanup_regex = {
# This matches everything except spaces, parens, colon, comma, and period
'most_punctuations': re.compile(r'([^\.():,\s]+)$'),
# This matches everything except a space.
- 'all_punctuations': re.compile('([^\s]+)$'),
+ 'all_punctuations': re.compile(r'([^\s]+)$'),
}
def last_word(text, include='alphanum_underscore'):
- """
+ r"""
Find the last word in a sentence.
>>> last_word('abc')
diff --git a/mycli/packages/special/iocommands.py b/mycli/packages/special/iocommands.py
index 11dca8d..58066b8 100644
--- a/mycli/packages/special/iocommands.py
+++ b/mycli/packages/special/iocommands.py
@@ -8,6 +8,7 @@ from io import open
from time import sleep
import click
+import pyperclip
import sqlparse
from . import export
@@ -23,6 +24,8 @@ PAGER_ENABLED = True
tee_file = None
once_file = None
written_to_once_file = False
+pipe_once_process = None
+written_to_pipe_once_process = False
delimiter_command = DelimiterCommand()
@@ -115,7 +118,7 @@ def get_editor_query(sql):
# The reason we can't simply do .strip('\e') is that it strips characters,
# not a substring. So it'll strip "e" in the end of the sql also!
# Ex: "select * from style\e" -> "select * from styl".
- pattern = re.compile('(^\\\e|\\\e$)')
+ pattern = re.compile(r'(^\\e|\\e$)')
while pattern.search(sql):
sql = pattern.sub('', sql)
@@ -159,6 +162,47 @@ def open_external_editor(filename=None, sql=None):
return (query, message)
+@export
+def clip_command(command):
+ """Is this a clip command?
+
+ :param command: string
+
+ """
+ # It is possible to have `\clip` or `SELECT * FROM \clip`. So we check
+ # for both conditions.
+ return command.strip().endswith('\\clip') or command.strip().startswith('\\clip')
+
+
+@export
+def get_clip_query(sql):
+ """Get the query part of a clip command."""
+ sql = sql.strip()
+
+ # The reason we can't simply do .strip('\clip') is that it strips characters,
+ # not a substring. So it'll strip "c" in the end of the sql also!
+ pattern = re.compile(r'(^\\clip|\\clip$)')
+ while pattern.search(sql):
+ sql = pattern.sub('', sql)
+
+ return sql
+
+
+@export
+def copy_query_to_clipboard(sql=None):
+ """Send query to the clipboard."""
+
+ sql = sql or ''
+ message = None
+
+ try:
+ pyperclip.copy(u'{sql}'.format(sql=sql))
+ except RuntimeError as e:
+ message = 'Error clipping query: %s.' % e.strerror
+
+ return message
+
+
@special_command('\\f', '\\f [name [args..]]', 'List or execute favorite queries.', arg_type=PARSED_QUERY, case_sensitive=True)
def execute_favorite_query(cur, arg, **_):
"""Returns (title, rows, headers, status)"""
@@ -213,7 +257,7 @@ def subst_favorite_query_args(query, args):
query = query.replace(subst_var, val)
- match = re.search('\\$\d+', query)
+ match = re.search(r'\$\d+', query)
if match:
return[None, 'missing substitution for ' + match.group(0) + ' in query:\n ' + query]
@@ -337,7 +381,11 @@ def write_tee(output):
def set_once(arg, **_):
global once_file, written_to_once_file
- once_file = parseargfile(arg)
+ try:
+ once_file = open(**parseargfile(arg))
+ except (IOError, OSError) as e:
+ raise OSError("Cannot write to file '{}': {}".format(
+ e.filename, e.strerror))
written_to_once_file = False
return [(None, None, None, "")]
@@ -347,26 +395,68 @@ def set_once(arg, **_):
def write_once(output):
global once_file, written_to_once_file
if output and once_file:
- try:
- f = open(**once_file)
- except (IOError, OSError) as e:
- once_file = None
- raise OSError("Cannot write to file '{}': {}".format(
- e.filename, e.strerror))
- with f:
- click.echo(output, file=f, nl=False)
- click.echo(u"\n", file=f, nl=False)
+ click.echo(output, file=once_file, nl=False)
+ click.echo(u"\n", file=once_file, nl=False)
+ once_file.flush()
written_to_once_file = True
@export
def unset_once_if_written():
"""Unset the once file, if it has been written to."""
- global once_file
- if written_to_once_file:
+ global once_file, written_to_once_file
+ if written_to_once_file and once_file:
+ once_file.close()
once_file = None
+@special_command('\\pipe_once', '\\| command',
+ 'Send next result to a subprocess.',
+ aliases=('\\|', ))
+def set_pipe_once(arg, **_):
+ global pipe_once_process, written_to_pipe_once_process
+ pipe_once_cmd = shlex.split(arg)
+ if len(pipe_once_cmd) == 0:
+ raise OSError("pipe_once requires a command")
+ written_to_pipe_once_process = False
+ pipe_once_process = subprocess.Popen(pipe_once_cmd,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ bufsize=1,
+ encoding='UTF-8',
+ universal_newlines=True)
+ return [(None, None, None, "")]
+
+
+@export
+def write_pipe_once(output):
+ global pipe_once_process, written_to_pipe_once_process
+ if output and pipe_once_process:
+ try:
+ click.echo(output, file=pipe_once_process.stdin, nl=False)
+ click.echo(u"\n", file=pipe_once_process.stdin, nl=False)
+ except (IOError, OSError) as e:
+ pipe_once_process.terminate()
+ raise OSError(
+ "Failed writing to pipe_once subprocess: {}".format(e.strerror))
+ written_to_pipe_once_process = True
+
+
+@export
+def unset_pipe_once_if_written():
+ """Unset the pipe_once cmd, if it has been written to."""
+ global pipe_once_process, written_to_pipe_once_process
+ if written_to_pipe_once_process:
+ (stdout_data, stderr_data) = pipe_once_process.communicate()
+ if len(stdout_data) > 0:
+ print(stdout_data.rstrip(u"\n"))
+ if len(stderr_data) > 0:
+ print(stderr_data.rstrip(u"\n"))
+ pipe_once_process = None
+ written_to_pipe_once_process = False
+
+
@special_command(
'watch',
'watch [seconds] [-c] query',
diff --git a/mycli/packages/special/main.py b/mycli/packages/special/main.py
index dddba66..ab04f30 100644
--- a/mycli/packages/special/main.py
+++ b/mycli/packages/special/main.py
@@ -112,6 +112,8 @@ def quit(*_args):
@special_command('\\e', '\\e', 'Edit command with editor (uses $EDITOR).',
arg_type=NO_QUERY, case_sensitive=True)
+@special_command('\\clip', '\\clip', 'Copy query to the system clipboard.',
+ arg_type=NO_QUERY, case_sensitive=True)
@special_command('\\G', '\\G', 'Display current query results vertically.',
arg_type=NO_QUERY, case_sensitive=True)
def stub():
diff --git a/mycli/sqlcompleter.py b/mycli/sqlcompleter.py
index 20611be..73b9b44 100644
--- a/mycli/sqlcompleter.py
+++ b/mycli/sqlcompleter.py
@@ -59,7 +59,7 @@ class SQLCompleter(Completer):
self.reserved_words = set()
for x in self.keywords:
self.reserved_words.update(x.split())
- self.name_pattern = compile("^[_a-z][_a-z0-9\$]*$")
+ self.name_pattern = compile(r"^[_a-z][_a-z0-9\$]*$")
self.special_commands = []
self.table_formats = supported_formats
diff --git a/mycli/sqlexecute.py b/mycli/sqlexecute.py
index c68af0f..7534982 100644
--- a/mycli/sqlexecute.py
+++ b/mycli/sqlexecute.py
@@ -42,7 +42,7 @@ class SQLExecute(object):
def __init__(self, database, user, password, host, port, socket, charset,
local_infile, ssl, ssh_user, ssh_host, ssh_port, ssh_password,
- ssh_key_filename):
+ ssh_key_filename, init_command=None):
self.dbname = database
self.user = user
self.password = password
@@ -59,12 +59,13 @@ class SQLExecute(object):
self.ssh_port = ssh_port
self.ssh_password = ssh_password
self.ssh_key_filename = ssh_key_filename
+ self.init_command = init_command
self.connect()
def connect(self, database=None, user=None, password=None, host=None,
port=None, socket=None, charset=None, local_infile=None,
ssl=None, ssh_host=None, ssh_port=None, ssh_user=None,
- ssh_password=None, ssh_key_filename=None):
+ ssh_password=None, ssh_key_filename=None, init_command=None):
db = (database or self.dbname)
user = (user or self.user)
password = (password or self.password)
@@ -79,6 +80,7 @@ class SQLExecute(object):
ssh_port = (ssh_port or self.ssh_port)
ssh_password = (ssh_password or self.ssh_password)
ssh_key_filename = (ssh_key_filename or self.ssh_key_filename)
+ init_command = (init_command or self.init_command)
_logger.debug(
'Connection DB Params: \n'
'\tdatabase: %r'
@@ -93,9 +95,11 @@ class SQLExecute(object):
'\tssh_host: %r'
'\tssh_port: %r'
'\tssh_password: %r'
- '\tssh_key_filename: %r',
+ '\tssh_key_filename: %r'
+ '\tinit_command: %r',
db, user, host, port, socket, charset, local_infile, ssl,
- ssh_user, ssh_host, ssh_port, ssh_password, ssh_key_filename
+ ssh_user, ssh_host, ssh_port, ssh_password, ssh_key_filename,
+ init_command
)
conv = conversions.copy()
conv.update({
@@ -110,12 +114,16 @@ class SQLExecute(object):
if ssh_host:
defer_connect = True
+ client_flag = pymysql.constants.CLIENT.INTERACTIVE
+ if init_command and len(list(special.split_queries(init_command))) > 1:
+ client_flag |= pymysql.constants.CLIENT.MULTI_STATEMENTS
+
conn = pymysql.connect(
database=db, user=user, password=password, host=host, port=port,
unix_socket=socket, use_unicode=True, charset=charset,
- autocommit=True, client_flag=pymysql.constants.CLIENT.INTERACTIVE,
+ autocommit=True, client_flag=client_flag,
local_infile=local_infile, conv=conv, ssl=ssl, program_name="mycli",
- defer_connect=defer_connect
+ defer_connect=defer_connect, init_command=init_command
)
if ssh_host:
@@ -146,6 +154,7 @@ class SQLExecute(object):
self.socket = socket
self.charset = charset
self.ssl = ssl
+ self.init_command = init_command
# retrieve connection id
self.reset_connection_id()
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 8e206a5..7a38ed5 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -3,8 +3,8 @@ pytest!=3.3.0
pytest-cov==2.4.0
tox
twine==1.12.1
-behave
-pexpect
+behave>=1.2.4
+pexpect==3.3
coverage==5.0.4
codecov==2.0.9
autopep8==1.3.3
diff --git a/setup.py b/setup.py
index 156cd1a..4aa7f91 100755
--- a/setup.py
+++ b/setup.py
@@ -19,12 +19,13 @@ description = 'CLI for MySQL Database. With auto-completion and syntax highlight
install_requirements = [
'click >= 7.0',
'Pygments >= 1.6',
- 'prompt_toolkit>=3.0.0,<4.0.0',
+ 'prompt_toolkit>=3.0.6,<4.0.0',
'PyMySQL >= 0.9.2',
'sqlparse>=0.3.0,<0.4.0',
'configobj >= 5.0.5',
'cryptography >= 1.0.0',
- 'cli_helpers[styles] > 1.1.0',
+ 'cli_helpers[styles] >= 2.0.1',
+ 'pyperclip >= 1.8.1'
]
@@ -65,7 +66,7 @@ class test(TestCommand):
def initialize_options(self):
TestCommand.initialize_options(self)
self.pytest_args = ''
- self.behave_args = ''
+ self.behave_args = '--no-capture'
def run_tests(self):
unit_test_errno = subprocess.call(
@@ -76,6 +77,7 @@ class test(TestCommand):
'behave test/features ' + self.behave_args,
shell=True
)
+ subprocess.run(['git', 'checkout', '--', 'test/myclirc'], check=False)
sys.exit(unit_test_errno or cli_errno)
diff --git a/test/conftest.py b/test/conftest.py
index cf6d721..d7d10ce 100644
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -4,7 +4,7 @@ from .utils import (HOST, USER, PASSWORD, PORT, CHARSET, create_db,
import mycli.sqlexecute
-@pytest.yield_fixture(scope="function")
+@pytest.fixture(scope="function")
def connection():
create_db('_test_db')
connection = db_connection('_test_db')
diff --git a/test/features/crud_database.feature b/test/features/crud_database.feature
index 0c298b6..f4a7a7f 100644
--- a/test/features/crud_database.feature
+++ b/test/features/crud_database.feature
@@ -16,6 +16,10 @@ Feature: manipulate databases:
when we connect to dbserver
then we see database connected
+ Scenario: connect and disconnect from quoted test database
+ When we connect to quoted test database
+ then we see database connected
+
Scenario: create and drop default database
When we create database
then we see database created
diff --git a/test/features/db_utils.py b/test/features/db_utils.py
index c29dedb..be550e9 100644
--- a/test/features/db_utils.py
+++ b/test/features/db_utils.py
@@ -1,11 +1,12 @@
import pymysql
-def create_db(hostname='localhost', username=None, password=None,
- dbname=None):
+def create_db(hostname='localhost', port=3306, username=None,
+ password=None, dbname=None):
"""Create test database.
:param hostname: string
+ :param port: int
:param username: string
:param password: string
:param dbname: string
@@ -14,6 +15,7 @@ def create_db(hostname='localhost', username=None, password=None,
"""
cn = pymysql.connect(
host=hostname,
+ port=port,
user=username,
password=password,
charset='utf8mb4',
@@ -26,14 +28,15 @@ def create_db(hostname='localhost', username=None, password=None,
cn.close()
- cn = create_cn(hostname, password, username, dbname)
+ cn = create_cn(hostname, port, password, username, dbname)
return cn
-def create_cn(hostname, password, username, dbname):
+def create_cn(hostname, port, password, username, dbname):
"""Open connection to database.
:param hostname:
+ :param port:
:param password:
:param username:
:param dbname: string
@@ -42,6 +45,7 @@ def create_cn(hostname, password, username, dbname):
"""
cn = pymysql.connect(
host=hostname,
+ port=port,
user=username,
password=password,
db=dbname,
@@ -52,11 +56,12 @@ def create_cn(hostname, password, username, dbname):
return cn
-def drop_db(hostname='localhost', username=None, password=None,
- dbname=None):
+def drop_db(hostname='localhost', port=3306, username=None,
+ password=None, dbname=None):
"""Drop database.
:param hostname: string
+ :param port: int
:param username: string
:param password: string
:param dbname: string
@@ -64,6 +69,7 @@ def drop_db(hostname='localhost', username=None, password=None,
"""
cn = pymysql.connect(
host=hostname,
+ port=port,
user=username,
password=password,
db=dbname,
diff --git a/test/features/environment.py b/test/features/environment.py
index 1a49dbe..98c2004 100644
--- a/test/features/environment.py
+++ b/test/features/environment.py
@@ -16,8 +16,9 @@ def before_all(context):
os.environ['LINES'] = "100"
os.environ['COLUMNS'] = "100"
os.environ['EDITOR'] = 'ex'
- os.environ['LC_ALL'] = 'en_US.utf8'
+ os.environ['LC_ALL'] = 'en_US.UTF-8'
os.environ['PROMPT_TOOLKIT_NO_CPR'] = '1'
+ os.environ['MYCLI_HISTFILE'] = os.devnull
test_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
login_path_file = os.path.join(test_dir, 'mylogin.cnf')
@@ -42,6 +43,10 @@ def before_all(context):
'my_test_host',
os.getenv('PYTEST_HOST', 'localhost')
),
+ 'port': context.config.userdata.get(
+ 'my_test_port',
+ int(os.getenv('PYTEST_PORT', '3306'))
+ ),
'user': context.config.userdata.get(
'my_test_user',
os.getenv('PYTEST_USER', 'root')
@@ -72,7 +77,8 @@ def before_all(context):
context.conf['myclirc'] = os.path.join(context.package_root, 'test',
'myclirc')
- context.cn = dbutils.create_db(context.conf['host'], context.conf['user'],
+ context.cn = dbutils.create_db(context.conf['host'], context.conf['port'],
+ context.conf['user'],
context.conf['pass'],
context.conf['dbname'])
@@ -82,8 +88,9 @@ def before_all(context):
def after_all(context):
"""Unset env parameters."""
dbutils.close_cn(context.cn)
- dbutils.drop_db(context.conf['host'], context.conf['user'],
- context.conf['pass'], context.conf['dbname'])
+ dbutils.drop_db(context.conf['host'], context.conf['port'],
+ context.conf['user'], context.conf['pass'],
+ context.conf['dbname'])
# Restore env vars.
#for k, v in context.pgenv.items():
@@ -118,11 +125,12 @@ def after_scenario(context, _):
host = context.conf['host']
dbname = context.currentdb
context.cli.expect_exact(
- '{0}@{1}:{2}> '.format(
+ '{0}@{1}:{2}>'.format(
user, host, dbname
),
timeout=5
)
+ context.cli.sendcontrol('c')
context.cli.sendcontrol('d')
context.cli.expect_exact(pexpect.EOF, timeout=5)
diff --git a/test/features/fixture_data/help_commands.txt b/test/features/fixture_data/help_commands.txt
index 657db7d..2c06d5d 100644
--- a/test/features/fixture_data/help_commands.txt
+++ b/test/features/fixture_data/help_commands.txt
@@ -2,6 +2,7 @@
| Command | Shortcut | Description |
+-------------+----------------------------+------------------------------------------------------------+
| \G | \G | Display current query results vertically. |
+| \clip | \clip | Copy query to the system clipboard. |
| \dt | \dt[+] [table] | List or describe tables. |
| \e | \e | Edit command with editor (uses $EDITOR). |
| \f | \f [name [args..]] | List or execute favorite queries. |
@@ -9,6 +10,7 @@
| \fs | \fs name query | Save a favorite query. |
| \l | \l | List databases. |
| \once | \o [-o] filename | Append next result to an output file (overwrite using -o). |
+| \pipe_once | \| command | Send next result to a subprocess. |
| \timing | \t | Toggle timing of commands. |
| connect | \r | Reconnect to the database. Optional database argument. |
| exit | \q | Exit. |
diff --git a/test/features/steps/crud_database.py b/test/features/steps/crud_database.py
index a0bfa53..841f37d 100644
--- a/test/features/steps/crud_database.py
+++ b/test/features/steps/crud_database.py
@@ -37,6 +37,14 @@ def step_db_connect_test(context):
context.cli.sendline('use {0};'.format(db_name))
+@when('we connect to quoted test database')
+def step_db_connect_quoted_tmp(context):
+ """Send connect to database."""
+ db_name = context.conf['dbname']
+ context.currentdb = db_name
+ context.cli.sendline('use `{0}`;'.format(db_name))
+
+
@when('we connect to tmp database')
def step_db_connect_tmp(context):
"""Send connect to database."""
@@ -64,15 +72,13 @@ def step_see_prompt(context):
user = context.conf['user']
host = context.conf['host']
dbname = context.currentdb
- wrappers.expect_exact(context, '{0}@{1}:{2}> '.format(
- user, host, dbname), timeout=5)
- context.atprompt = True
+ wrappers.wait_prompt(context, '{0}@{1}:{2}> '.format(user, host, dbname))
@then('we see help output')
def step_see_help(context):
for expected_line in context.fixture_data['help_commands.txt']:
- wrappers.expect_exact(context, expected_line + '\r\n', timeout=1)
+ wrappers.expect_exact(context, expected_line, timeout=1)
@then('we see database created')
@@ -96,10 +102,7 @@ def step_see_db_dropped_no_default(context):
context.currentdb = None
wrappers.expect_exact(context, 'Query OK, 0 rows affected', timeout=2)
- wrappers.expect_exact(context, '{0}@{1}:{2}> '.format(
- user, host, database), timeout=5)
-
- context.atprompt = True
+ wrappers.wait_prompt(context, '{0}@{1}:{2}>'.format(user, host, database))
@then('we see database connected')
diff --git a/test/features/steps/wrappers.py b/test/features/steps/wrappers.py
index 565ca59..de833dd 100644
--- a/test/features/steps/wrappers.py
+++ b/test/features/steps/wrappers.py
@@ -88,7 +88,7 @@ def wait_prompt(context, prompt=None):
user = context.conf['user']
host = context.conf['host']
dbname = context.currentdb
- prompt = '{0}@{1}:{2}> '.format(
+ prompt = '{0}@{1}:{2}>'.format(
user, host, dbname),
expect_exact(context, prompt, timeout=5)
context.atprompt = True
diff --git a/test/test_main.py b/test/test_main.py
index 3f92bd1..707c359 100644
--- a/test/test_main.py
+++ b/test/test_main.py
@@ -492,3 +492,37 @@ def test_ssh_config(monkeypatch):
MockMyCli.connect_args["ssh_host"] == "arg_host" and \
MockMyCli.connect_args["ssh_port"] == 3 and \
MockMyCli.connect_args["ssh_key_filename"] == "/path/to/key"
+
+
+@dbtest
+def test_init_command_arg(executor):
+ init_command = "set sql_select_limit=1000"
+ sql = 'show variables like "sql_select_limit";'
+ runner = CliRunner()
+ result = runner.invoke(
+ cli, args=CLI_ARGS + ["--init-command", init_command], input=sql
+ )
+
+ expected = "sql_select_limit\t1000\n"
+ assert result.exit_code == 0
+ assert expected in result.output
+
+
+@dbtest
+def test_init_command_multiple_arg(executor):
+ init_command = 'set sql_select_limit=2000; set max_join_size=20000'
+ sql = (
+ 'show variables like "sql_select_limit";\n'
+ 'show variables like "max_join_size"'
+ )
+ runner = CliRunner()
+ result = runner.invoke(
+ cli, args=CLI_ARGS + ['--init-command', init_command], input=sql
+ )
+
+ expected_sql_select_limit = 'sql_select_limit\t2000\n'
+ expected_max_join_size = 'max_join_size\t20000\n'
+
+ assert result.exit_code == 0
+ assert expected_sql_select_limit in result.output
+ assert expected_max_join_size in result.output
diff --git a/test/test_special_iocommands.py b/test/test_special_iocommands.py
index b8b3acb..73bfbab 100644
--- a/test/test_special_iocommands.py
+++ b/test/test_special_iocommands.py
@@ -49,7 +49,8 @@ def test_editor_command():
assert mycli.packages.special.get_filename(r'\e filename') == "filename"
os.environ['EDITOR'] = 'true'
- mycli.packages.special.open_external_editor(r'select 1') == "select 1"
+ os.environ['VISUAL'] = 'true'
+ mycli.packages.special.open_external_editor(sql=r'select 1') == "select 1"
def test_tee_command():
@@ -93,9 +94,8 @@ def test_once_command():
with pytest.raises(TypeError):
mycli.packages.special.execute(None, u"\\once")
- mycli.packages.special.execute(None, u"\\once /proc/access-denied")
with pytest.raises(OSError):
- mycli.packages.special.write_once(u"hello world")
+ mycli.packages.special.execute(None, u"\\once /proc/access-denied")
mycli.packages.special.write_once(u"hello world") # write without file set
with tempfile.NamedTemporaryFile() as f:
@@ -104,9 +104,24 @@ def test_once_command():
assert f.read() == b"hello world\n"
mycli.packages.special.execute(None, u"\\once -o " + f.name)
- mycli.packages.special.write_once(u"hello world")
+ mycli.packages.special.write_once(u"hello world line 1")
+ mycli.packages.special.write_once(u"hello world line 2")
f.seek(0)
- assert f.read() == b"hello world\n"
+ assert f.read() == b"hello world line 1\nhello world line 2\n"
+
+
+def test_pipe_once_command():
+ with pytest.raises(IOError):
+ mycli.packages.special.execute(None, u"\\pipe_once")
+
+ with pytest.raises(OSError):
+ mycli.packages.special.execute(
+ None, u"\\pipe_once /proc/access-denied")
+
+ mycli.packages.special.execute(None, u"\\pipe_once wc")
+ mycli.packages.special.write_once(u"hello world")
+ mycli.packages.special.unset_pipe_once_if_written()
+ # how to assert on wc output?
def test_parseargfile():
diff --git a/test/test_sqlexecute.py b/test/test_sqlexecute.py
index c2d38be..5168bf6 100644
--- a/test/test_sqlexecute.py
+++ b/test/test_sqlexecute.py
@@ -166,7 +166,7 @@ def test_favorite_query_expanded_output(executor):
results = run(executor, "\\fs test-ae select * from test")
assert_result_equal(results, status='Saved.')
- results = run(executor, "\\f test-ae \G")
+ results = run(executor, "\\f test-ae \\G")
assert is_expanded_output() is True
assert_result_equal(results, title='> select * from test',
headers=['a'], rows=[('abc',)], auto_status=False)
diff --git a/test/test_tabular_output.py b/test/test_tabular_output.py
index 7d7d000..c20c7de 100644
--- a/test/test_tabular_output.py
+++ b/test/test_tabular_output.py
@@ -16,7 +16,7 @@ from pymysql.constants import FIELD_TYPE
@pytest.fixture
def mycli():
cli = MyCli()
- cli.connect(None, USER, PASSWORD, HOST, PORT, None)
+ cli.connect(None, USER, PASSWORD, HOST, PORT, None, init_command=None)
return cli