From 5a07d42d34e8acd1374a0dfa41b2671768b7ee72 Mon Sep 17 00:00:00 2001 From: Roland Walker Date: Fri, 30 Jan 2026 07:13:27 -0500 Subject: [PATCH 1/4] Show user in password prompt (fixing regression) In order to do this, we have to lose some special DSN logic. There is just no way under the current setup, since for instance the SSL params get parsed out of the DSN before calling connect(). Once solution could be to do all configuration parsing early, a long- term goal. Here we replace the DSN fallthrough with a warning message. The password_from_file logic is incidentally improved. --- changelog.md | 1 + mycli/main.py | 93 ++++++++++++++++++++++------------------------- test/test_main.py | 3 +- 3 files changed, 47 insertions(+), 50 deletions(-) diff --git a/changelog.md b/changelog.md index 3644bcf2..1442ac0f 100644 --- a/changelog.md +++ b/changelog.md @@ -11,6 +11,7 @@ Bug Fixes -------- * Refactor completions for special commands, with minor casing fixes. * Raise `--password-file` higher in the precedence of password specification. +* Fix regression: show username in password prompt. Internal diff --git a/mycli/main.py b/mycli/main.py index 0e3e37d3..ee5a2819 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -480,6 +480,7 @@ def connect( ssh_key_filename: str | None = "", init_command: str | None = "", unbuffered: bool | None = None, + password_file: str | None = None, ) -> None: cnf = { "database": None, @@ -538,9 +539,50 @@ def connect( # 4. DSN (mysql://user:password) # 5. cnf (.my.cnf / etc) + def get_password_from_file(password_file: str | None) -> str | None: + if not password_file: + return None + try: + with open(password_file) as fp: + password = fp.readline().strip() + return password + except FileNotFoundError: + click.secho(f"Password file '{password_file}' not found", err=True, fg="red") + sys.exit(1) + except PermissionError: + click.secho(f"Permission denied reading password file '{password_file}'", err=True, fg="red") + sys.exit(1) + except IsADirectoryError: + click.secho(f"Path '{password_file}' is a directory, not a file", err=True, fg="red") + sys.exit(1) + except Exception as e: + click.secho(f"Error reading password file '{password_file}': {str(e)}", err=True, fg="red") + sys.exit(1) + + if passwd and '://' in passwd: + is_valid_scheme, _scheme = is_valid_connection_scheme(passwd or '') + if is_valid_scheme: + click.secho('Warning: password looks like a DSN. Check the command line.', err=True, fg='yellow') + + # if user passes the --p* flag, ask for the password right away + # to enforce consistency and reduce lag as much as possible + if passwd == "MYCLI_ASK_PASSWORD": + passwd = click.prompt(f"Enter password for {user}", hide_input=True, show_default=False, default='', type=str, err=True) + + # if the passwd is not specified try to set it using the password_file option + if passwd is None and password_file: + password_from_file = get_password_from_file(password_file) + if password_from_file is not None: + passwd = password_from_file + + # getting the envvar ourselves because the envvar from a click + # option cannot be an empty string, but a password can be + if passwd is None and os.environ.get("MYSQL_PWD") is not None: + passwd = os.environ.get("MYSQL_PWD") + # 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) + passwd = click.prompt(f"Enter password for {user}", hide_input=True, show_default=False, default='', type=str, err=True) # Connect to the database. def _connect() -> None: @@ -1600,54 +1642,6 @@ def cli( - mycli mysql://my_user@my_host.com:3306/my_database """ - - def get_password_from_file(password_file: str | None) -> str | None: - if not password_file: - return None - try: - with open(password_file) as fp: - password = fp.readline().strip() - return password - except FileNotFoundError: - click.secho(f"Password file '{password_file}' not found", err=True, fg="red") - sys.exit(1) - except PermissionError: - click.secho(f"Permission denied reading password file '{password_file}'", err=True, fg="red") - sys.exit(1) - except IsADirectoryError: - click.secho(f"Path '{password_file}' is a directory, not a file", err=True, fg="red") - sys.exit(1) - except Exception as e: - click.secho(f"Error reading password file '{password_file}': {str(e)}", err=True, fg="red") - sys.exit(1) - - # 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 "://" in password: - # check if the scheme is valid. We do not actually have any logic for these, but - # it will most usefully catch the case where we erroneously catch someone's - # password, and give them an easy error message to follow / report - is_valid_scheme, scheme = is_valid_connection_scheme(password) - if not is_valid_scheme: - click.secho(f"Error: Unknown connection scheme provided for DSN URI ({scheme}://)", err=True, fg="red") - sys.exit(1) - database = password - password = click.prompt("Enter password", hide_input=True, show_default=False, default='', type=str, err=True) - - # if the passwd is not specified try to set it using the password_file option - if password is None and password_file: - if password_from_file := get_password_from_file(password_file): - password = password_from_file - - # getting the envvar ourselves because the envvar from a click - # option cannot be an empty string, but a password can be - if password is None and os.environ.get("MYSQL_PWD") is not None: - password = os.environ.get("MYSQL_PWD") - mycli = MyCli( prompt=prompt, logfile=logfile, @@ -1880,6 +1874,7 @@ def get_password_from_file(password_file: str | None) -> str | None: init_command=combined_init_cmd, unbuffered=unbuffered, charset=charset, + password_file=password_file, ) if combined_init_cmd: diff --git a/test/test_main.py b/test/test_main.py index 451277a4..eb7b0683 100644 --- a/test/test_main.py +++ b/test/test_main.py @@ -11,7 +11,8 @@ from click.testing import CliRunner from pymysql.err import OperationalError -from mycli.main import MyCli, cli, is_valid_connection_scheme, thanks_picker +from mycli.main import MyCli, cli, thanks_picker +from mycli.packages.parseutils import is_valid_connection_scheme import mycli.packages.special from mycli.packages.special.main import COMMANDS as SPECIAL_COMMANDS from mycli.sqlexecute import ServerInfo, SQLExecute From 2577d07e5ccfb79056291ab5e62bed628f5c3804 Mon Sep 17 00:00:00 2001 From: Scott Nemes Date: Sat, 31 Jan 2026 16:46:07 -0800 Subject: [PATCH 2/4] Rework password logic to allow for user in prompt and keep previous functionality --- mycli/main.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/mycli/main.py b/mycli/main.py index ee5a2819..1a109de3 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -501,10 +501,23 @@ def connect( } cnf = self.read_my_cnf(self.my_cnf, list(cnf.keys())) + # get user and passwd from config so we can use it in password checking logic and prompt + user = user or cnf["user"] or os.getenv("USER") + passwd = passwd if isinstance(passwd, str) else cnf["password"] + + if database is None and passwd is not None and "://" in passwd: + # check if the scheme is valid. We do not actually have any logic for these, but + # it will most usefully catch the case where we erroneously catch someone's + # password, and give them an easy error message to follow / report + is_valid_scheme, scheme = is_valid_connection_scheme(passwd) + if not is_valid_scheme: + click.secho(f"Error: Unknown connection scheme provided for DSN URI ({scheme}://)", err=True, fg="red") + sys.exit(1) + database = passwd + passwd = click.prompt(f"Enter password for {user}", hide_input=True, show_default=False, default='', type=str, err=True) # Fall back to config values only if user did not specify a value. database = database or cnf["database"] - user = user or cnf["user"] or os.getenv("USER") host = host or cnf["host"] port = port or cnf["port"] ssl_config: dict[str, Any] = ssl or {} @@ -515,7 +528,6 @@ def connect( if not host or host == "localhost": socket = socket or cnf["socket"] or cnf["default_socket"] or guess_socket_location() - passwd = passwd if isinstance(passwd, str) else cnf["password"] charset = charset or self.config["main"].get("default_character_set") or cnf["default-character-set"] or "utf8mb4" # Favor whichever local_infile option is set. @@ -559,13 +571,6 @@ def get_password_from_file(password_file: str | None) -> str | None: click.secho(f"Error reading password file '{password_file}': {str(e)}", err=True, fg="red") sys.exit(1) - if passwd and '://' in passwd: - is_valid_scheme, _scheme = is_valid_connection_scheme(passwd or '') - if is_valid_scheme: - click.secho('Warning: password looks like a DSN. Check the command line.', err=True, fg='yellow') - - # if user passes the --p* flag, ask for the password right away - # to enforce consistency and reduce lag as much as possible if passwd == "MYCLI_ASK_PASSWORD": passwd = click.prompt(f"Enter password for {user}", hide_input=True, show_default=False, default='', type=str, err=True) From 28edca334b423f33bea79c3ca25e52c91551b889 Mon Sep 17 00:00:00 2001 From: Scott Nemes Date: Sun, 1 Feb 2026 09:28:24 -0800 Subject: [PATCH 3/4] Moved passwd from cnf line down --- mycli/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mycli/main.py b/mycli/main.py index 1a109de3..afca104a 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -501,9 +501,8 @@ def connect( } cnf = self.read_my_cnf(self.my_cnf, list(cnf.keys())) - # get user and passwd from config so we can use it in password checking logic and prompt + # get user from config so we can use it in password checking logic and prompt user = user or cnf["user"] or os.getenv("USER") - passwd = passwd if isinstance(passwd, str) else cnf["password"] if database is None and passwd is not None and "://" in passwd: # check if the scheme is valid. We do not actually have any logic for these, but @@ -528,6 +527,7 @@ def connect( if not host or host == "localhost": socket = socket or cnf["socket"] or cnf["default_socket"] or guess_socket_location() + passwd = passwd if isinstance(passwd, str) else cnf["password"] charset = charset or self.config["main"].get("default_character_set") or cnf["default-character-set"] or "utf8mb4" # Favor whichever local_infile option is set. From 66db65eb26a058a74e63b4726040f5a839933a18 Mon Sep 17 00:00:00 2001 From: Scott Nemes Date: Sun, 1 Feb 2026 15:44:26 -0800 Subject: [PATCH 4/4] Fix DSN parsing --- mycli/main.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/mycli/main.py b/mycli/main.py index afca104a..bf91020d 100755 --- a/mycli/main.py +++ b/mycli/main.py @@ -501,22 +501,10 @@ def connect( } cnf = self.read_my_cnf(self.my_cnf, list(cnf.keys())) - # get user from config so we can use it in password checking logic and prompt - user = user or cnf["user"] or os.getenv("USER") - - if database is None and passwd is not None and "://" in passwd: - # check if the scheme is valid. We do not actually have any logic for these, but - # it will most usefully catch the case where we erroneously catch someone's - # password, and give them an easy error message to follow / report - is_valid_scheme, scheme = is_valid_connection_scheme(passwd) - if not is_valid_scheme: - click.secho(f"Error: Unknown connection scheme provided for DSN URI ({scheme}://)", err=True, fg="red") - sys.exit(1) - database = passwd - passwd = click.prompt(f"Enter password for {user}", hide_input=True, show_default=False, default='', type=str, err=True) # Fall back to config values only if user did not specify a value. database = database or cnf["database"] + user = user or cnf["user"] or os.getenv("USER") host = host or cnf["host"] port = port or cnf["port"] ssl_config: dict[str, Any] = ssl or {} @@ -1647,6 +1635,17 @@ def cli( - mycli mysql://my_user@my_host.com:3306/my_database """ + if database is None and password is not None and "://" in password: + # check if the scheme is valid. We do not actually have any logic for these, but + # it will most usefully catch the case where we erroneously catch someone's + # password, and give them an easy error message to follow / report + is_valid_scheme, scheme = is_valid_connection_scheme(password) + if not is_valid_scheme: + click.secho(f"Error: Unknown connection scheme provided for DSN URI ({scheme}://)", err=True, fg="red") + sys.exit(1) + database = password + password = "MYCLI_ASK_PASSWORD" + mycli = MyCli( prompt=prompt, logfile=logfile,