Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 6 additions & 1 deletion mycli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
from mycli.main_modes.list_dsn import main_list_dsn
from mycli.main_modes.list_ssh_config import main_list_ssh_config
from mycli.main_modes.repl import get_prompt, main_repl, set_all_external_titles
from mycli.packages import special
from mycli.packages import cli_utils, special
from mycli.packages.cli_utils import filtered_sys_argv, is_valid_connection_scheme
from mycli.packages.filepaths import dir_path_exists, guess_socket_location
from mycli.packages.interactive_utils import confirm_destructive_query
Expand Down Expand Up @@ -1490,6 +1490,11 @@ 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)

# pick up a password that was extracted from argv before Click parsing
# (passwords starting with '-' can't survive Click's option parsing)
if cli_args.password is None and cli_utils._extracted_password is not None:
cli_args.password = cli_utils._extracted_password

# if the password value looks like a DSN, treat it as such and
# prompt for password
if cli_args.database is None and isinstance(cli_args.password, str) and "://" in cli_args.password:
Expand Down
60 changes: 59 additions & 1 deletion mycli/packages/cli_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,70 @@

import sys

# Stash for a password extracted from argv before Click parsing.
# Click cannot handle passwords that start with a dash (e.g. --password -mypass)
# because it interprets them as option flags. _normalize_password_args strips
# such values from argv and stores them here so click_entrypoint can pick them up.
_extracted_password: str | None = None


def filtered_sys_argv() -> list[str]:
args = sys.argv[1:]
if args == ['-h']:
args = ['--help']
return args
return _normalize_password_args(args)


def _normalize_password_args(args: list[str]) -> list[str]:
"""Extract --password/--pass/-p values that start with a dash before Click
sees them.

Click treats tokens starting with "-" as option flags, so
"--password -mypass" and "-p-mypass" fail. This function removes the
password from the arg list and stashes it in "_extracted_password" for
later retrieval.

Handled forms:
- "--password -mypass" / "--pass -mypass" / "-p -mypass"
- "--password=-mypass" / "--pass=-mypass"
- "-p-mypass"
"""
global _extracted_password
_extracted_password = None

result: list[str] = []
i = 0
while i < len(args):
arg = args[i]

# --password=-mypass / --pass=-mypass
for prefix in ('--password=', '--pass='):
if arg.startswith(prefix):
value = arg[len(prefix) :]
if value.startswith('-'):
_extracted_password = value
break
else:
# -p-mypass (short option with dash-prefixed value glued on)
if arg.startswith('-p-'):
_extracted_password = arg[2:]
i += 1
continue

# "--password -mypass" / "--pass -mypass" (two separate tokens)
if arg in ('--password', '--pass') and i + 1 < len(args):
next_arg = args[i + 1]
if next_arg.startswith('-') and next_arg != '--':
_extracted_password = next_arg
i += 2
continue

result.append(arg)
i += 1
continue
i += 1

return result


def is_valid_connection_scheme(text: str) -> tuple[bool, str | None]:
Expand Down
33 changes: 33 additions & 0 deletions test/pytests/test_cli_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from mycli.packages import cli_utils
from mycli.packages.cli_utils import (
_normalize_password_args,
filtered_sys_argv,
is_valid_connection_scheme,
)
Expand Down Expand Up @@ -37,3 +38,35 @@ def test_filtered_sys_argv(monkeypatch, argv, expected):
)
def test_is_valid_connection_scheme(text, is_valid, invalid_scheme):
assert is_valid_connection_scheme(text) == (is_valid, invalid_scheme)


@pytest.mark.parametrize(
('args', 'expected_args', 'expected_password'),
[
# --password / --pass with a dash-prefixed value: extracted from args
(['--password', '-mypass'], [], '-mypass'),
(['--pass', '-mypass'], [], '-mypass'),
# --password=-mypass / --pass=-mypass: extracted from args
(['--password=-mypass'], [], '-mypass'),
(['--pass=-mypass'], [], '-mypass'),
# -p-mypass: extracted from args
(['-p-mypass'], [], '-mypass'),
# --password with a normal value is left for Click
(['--password', 'mypass'], ['--password', 'mypass'], None),
(['--password=mypass'], ['--password=mypass'], None),
# --password with -- (end of options) is left alone
(['--password', '--'], ['--password', '--'], None),
# --password at end of args (used as flag) is left alone
(['--password'], ['--password'], None),
# -p at end of args (used as flag) is left alone
(['-p'], ['-p'], None),
# other args are preserved, only the password pair is extracted
(['-u', 'root', '--password', '-mypass', '-h', 'localhost'], ['-u', 'root', '-h', 'localhost'], '-mypass'),
(['-u', 'root', '-p-mypass', '-h', 'localhost'], ['-u', 'root', '-h', 'localhost'], '-mypass'),
# -p as a flag does not absorb the next option
(['-p', '-u', 'root'], ['-p', '-u', 'root'], None),
],
)
def test_normalize_password_args(args, expected_args, expected_password):
assert _normalize_password_args(args) == expected_args
assert cli_utils._extracted_password == expected_password
Loading