Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
8ff1879
Reworked password logic. Saving before things get more weird.
scottnemes Jan 7, 2026
c19bdb7
Tweaking defaults to try and make all the password options to play nice.
scottnemes Jan 7, 2026
9bc7a60
Updated tests
scottnemes Jan 9, 2026
44e3b03
Merge remote-tracking branch 'origin/main' into feat/341/rework-passw…
scottnemes Jan 10, 2026
1732cf3
Merge remote-tracking branch 'origin/main' into feat/341/rework-passw…
scottnemes Jan 10, 2026
67eb501
Updated changelog. Added separate envvar handling for MYSQL_PWD.
scottnemes Jan 10, 2026
2fd8107
Fixed typo
scottnemes Jan 10, 2026
6945990
Removed final password prompt to mimic vendor client functionality no…
scottnemes Jan 11, 2026
f8ad468
Simplified the code a bit after realizing the vendor client behavior …
scottnemes Jan 12, 2026
005d257
Fixed envvar step to work with latest logic
scottnemes Jan 12, 2026
867f915
Fixed condition, didn't make sense
scottnemes Jan 12, 2026
da6ed82
Merge remote-tracking branch 'origin/main' into feat/341/rework-passw…
scottnemes Jan 12, 2026
dd2346f
Synced with main
scottnemes Jan 13, 2026
59f3870
Merge remote-tracking branch 'origin/main' into feat/341/rework-passw…
scottnemes Jan 16, 2026
ffe5178
Added custom click parsing class to determine when the password argum…
scottnemes Jan 17, 2026
ff4ad5e
Updated comments
scottnemes Jan 17, 2026
02698b4
Removed custom click class. Added check for DSN URI when password is …
scottnemes Jan 19, 2026
97f637a
Updated changelog
scottnemes Jan 20, 2026
02ecd6a
Synced from main
scottnemes Jan 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ TBD

Features
--------
* Make password options also function as flags. Reworked password logic to prompt user as early as possible (#341).
* More complete and up-to-date set of MySQL reserved words for completions.
* Place exact-leading completions first.
* Allow history file location to be configured.
Expand Down
104 changes: 44 additions & 60 deletions mycli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
from mycli.packages.tabular_output import sql_format
from mycli.packages.toolkit.history import FileHistoryWithTimestamp
from mycli.sqlcompleter import SQLCompleter
from mycli.sqlexecute import ERROR_CODE_ACCESS_DENIED, FIELD_TYPES, SQLExecute
from mycli.sqlexecute import FIELD_TYPES, SQLExecute

try:
import paramiko
Expand Down Expand Up @@ -460,7 +460,7 @@ def connect(
self,
database: str | None = "",
user: str | None = "",
passwd: str | None = "",
passwd: str | None = None,
host: str | None = "",
port: str | int | None = "",
socket: str | None = "",
Expand Down Expand Up @@ -528,10 +528,19 @@ def connect(
# if the passwd is not specified try to set it using the password_file option
password_from_file = self.get_password_from_file(password_file)
passwd = passwd if isinstance(passwd, str) else password_from_file
passwd = '' if passwd is None else passwd

# Connect to the database.
# password hierarchy
# 1. -p / --pass/--password CLI options
# 2. envvar (MYSQL_PWD)
# 3. DSN (mysql://user:password)
# 4. cnf (.my.cnf / etc)
# 5. --password-file CLI option

# if no password was found from all of the above sources, ask for a password
if passwd is None:
passwd = click.prompt("Enter password", hide_input=True, show_default=False, default='', type=str, err=True)

# Connect to the database.
def _connect() -> None:
try:
self.sqlexecute = SQLExecute(
Expand All @@ -552,31 +561,7 @@ def _connect() -> None:
init_command,
)
except pymysql.OperationalError as e1:
if e1.args[0] == ERROR_CODE_ACCESS_DENIED:
if password_from_file is not None:
new_passwd = password_from_file
else:
new_passwd = click.prompt(
f"Password for {user}", hide_input=True, show_default=False, default='', type=str, err=True
)
self.sqlexecute = SQLExecute(
database,
user,
new_passwd,
host,
int_port,
socket,
charset,
use_local_infile,
ssl_config_or_none,
ssh_user,
ssh_host,
int(ssh_port) if ssh_port else None,
ssh_password,
ssh_key_filename,
init_command,
)
elif e1.args[0] == HANDSHAKE_ERROR and ssl is not None and ssl.get("mode", None) == "auto":
if e1.args[0] == HANDSHAKE_ERROR and ssl is not None and ssl.get("mode", None) == "auto":
try:
self.sqlexecute = SQLExecute(
database,
Expand All @@ -595,33 +580,8 @@ def _connect() -> None:
ssh_key_filename,
init_command,
)
except pymysql.OperationalError as e2:
if e2.args[0] == ERROR_CODE_ACCESS_DENIED:
if password_from_file is not None:
new_passwd = password_from_file
else:
new_passwd = click.prompt(
f"Password for {user}", hide_input=True, show_default=False, default='', type=str, err=True
)
self.sqlexecute = SQLExecute(
database,
user,
new_passwd,
host,
int_port,
socket,
charset,
use_local_infile,
None,
ssh_user,
ssh_host,
int(ssh_port) if ssh_port else None,
ssh_password,
ssh_key_filename,
init_command,
)
else:
raise e2
except Exception as e2:
raise e2
else:
raise e1

Expand Down Expand Up @@ -1492,8 +1452,16 @@ def get_last_query(self) -> str | None:
@click.option("-P", "--port", envvar="MYSQL_TCP_PORT", type=int, help="Port number to use for connection. Honors $MYSQL_TCP_PORT.")
@click.option("-u", "--user", help="User name to connect to the database.")
@click.option("-S", "--socket", envvar="MYSQL_UNIX_PORT", help="The socket file to use for connection.")
@click.option("-p", "--password", "password", envvar="MYSQL_PWD", type=str, help="Password to connect to the database.")
@click.option("--pass", "password", envvar="MYSQL_PWD", type=str, help="Password to connect to the database.")
@click.option(
"-p",
"--pass",
"--password",
"password",
is_flag=False,
flag_value="MYCLI_ASK_PASSWORD",
type=str,
help="Prompt for (or enter in cleartext) password to connect to the database.",
)
@click.option("--ssh-user", help="User name to connect to ssh server.")
@click.option("--ssh-host", help="Host name to connect to ssh server.")
@click.option("--ssh-port", default=22, help="Port to connect to ssh server.")
Expand Down Expand Up @@ -1553,9 +1521,11 @@ def get_last_query(self) -> str | None:
@click.option(
"--password-file", type=click.Path(), help="File or FIFO path containing the password to connect to the db if not specified otherwise."
)
@click.argument("database", default="", nargs=1)
@click.argument("database", default=None, nargs=1)
@click.pass_context
def cli(
database: str,
ctx: click.Context,
database: str | None,
user: str | None,
host: str | None,
port: int | None,
Expand Down Expand Up @@ -1608,6 +1578,20 @@ def cli(
- mycli mysql://my_user@my_host.com:3306/my_database

"""
# if user passes the --p* flag, ask for the password right away
# to reduce lag as much as possible
if password == "MYCLI_ASK_PASSWORD":
password = click.prompt("Enter password", hide_input=True, show_default=False, default='', type=str, err=True)
# if the password value looks like a DSN, treat it as such and
# prompt for password
elif database is None and password is not None and password.startswith("mysql://"):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am still not sure about this, and predict we will get some issues filed. But we can try it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Were you expecting a different behavior? Thought this is what we talked about last. I could add back in the option ordering logic and only check for the DSN URI if the password option/flag is the last one, maybe that's what you were thinking?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your call!

database = password
password = click.prompt("Enter password", hide_input=True, show_default=False, default='', type=str, err=True)
# getting the envvar ourselves because the envvar from a click
# option cannot be an empty string, but a password can be
elif password is None and os.environ.get("MYSQL_PWD") is not None:
password = os.environ.get("MYSQL_PWD")

mycli = MyCli(
prompt=prompt,
logfile=logfile,
Expand Down
10 changes: 5 additions & 5 deletions test/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def test_ssl_mode_on(executor, capsys):
sql = "select * from performance_schema.session_status where variable_name = 'Ssl_cipher'"
result = runner.invoke(cli, args=CLI_ARGS + ["--csv", "--ssl-mode", ssl_mode], input=sql)
result_dict = next(csv.DictReader(result.stdout.split("\n")))
ssl_cipher = result_dict["VARIABLE_VALUE"]
ssl_cipher = result_dict.get("VARIABLE_VALUE", None)
assert ssl_cipher


Expand All @@ -58,7 +58,7 @@ def test_ssl_mode_auto(executor, capsys):
sql = "select * from performance_schema.session_status where variable_name = 'Ssl_cipher'"
result = runner.invoke(cli, args=CLI_ARGS + ["--csv", "--ssl-mode", ssl_mode], input=sql)
result_dict = next(csv.DictReader(result.stdout.split("\n")))
ssl_cipher = result_dict["VARIABLE_VALUE"]
ssl_cipher = result_dict.get("VARIABLE_VALUE", None)
assert ssl_cipher


Expand All @@ -69,7 +69,7 @@ def test_ssl_mode_off(executor, capsys):
sql = "select * from performance_schema.session_status where variable_name = 'Ssl_cipher'"
result = runner.invoke(cli, args=CLI_ARGS + ["--csv", "--ssl-mode", ssl_mode], input=sql)
result_dict = next(csv.DictReader(result.stdout.split("\n")))
ssl_cipher = result_dict["VARIABLE_VALUE"]
ssl_cipher = result_dict.get("VARIABLE_VALUE", None)
assert not ssl_cipher


Expand All @@ -80,7 +80,7 @@ def test_ssl_mode_overrides_ssl(executor, capsys):
sql = "select * from performance_schema.session_status where variable_name = 'Ssl_cipher'"
result = runner.invoke(cli, args=CLI_ARGS + ["--csv", "--ssl-mode", ssl_mode, "--ssl"], input=sql)
result_dict = next(csv.DictReader(result.stdout.split("\n")))
ssl_cipher = result_dict["VARIABLE_VALUE"]
ssl_cipher = result_dict.get("VARIABLE_VALUE", None)
assert not ssl_cipher


Expand All @@ -91,7 +91,7 @@ def test_ssl_mode_overrides_no_ssl(executor, capsys):
sql = "select * from performance_schema.session_status where variable_name = 'Ssl_cipher'"
result = runner.invoke(cli, args=CLI_ARGS + ["--csv", "--ssl-mode", ssl_mode, "--no-ssl"], input=sql)
result_dict = next(csv.DictReader(result.stdout.split("\n")))
ssl_cipher = result_dict["VARIABLE_VALUE"]
ssl_cipher = result_dict.get("VARIABLE_VALUE", None)
assert ssl_cipher


Expand Down