Skip to content

Commit e0bc634

Browse files
committed
allow CLI passwords starting with dash characters
Allow the argument to --password (and aliases) to start with a dash character, by stopping to pass the "flag_value" parameter to click. Now --password (and aliases) always take a value at the CLI, unless given in the final position, in which case the flag works as a request for an interactive prompt. This retains the mechanism of EMPTY_PASSWORD_FLAG_SENTINEL, just does not use "flag_value" to set the sentinel value. There still remains the special-casing of DSNs after --password flags, which, while not related to the dash-character bug, still defines some special strings which can't be used as CLI password arguments.
1 parent d38d6a0 commit e0bc634

6 files changed

Lines changed: 33 additions & 69 deletions

File tree

changelog.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ Features
77
* Make `--progress` and `--checkpoint` strictly by statement.
88
* Allow more characters in passwords read from a file.
99
* Show sponsors and contributors separately in startup messages.
10-
* Add support for expired password (sandbox) mode (#440)
10+
* Add support for expired password (sandbox) mode (#440).
11+
* Limit `--password` without an argument to the final position.
1112

1213

1314
Bug Fixes
@@ -22,6 +23,7 @@ Bug Fixes
2223
* Run empty `--execute` arguments instead of ignoring the flag.
2324
* Exit with error when the `--batch` argument is an empty string.
2425
* Avoid logging SSH passwords.
26+
* Allow passwords defined at the CLI to start with dash.
2527

2628

2729
Internal

mycli/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,5 @@
1717
# MySQL error codes not available in pymysql.constants.ER
1818
ER_MUST_CHANGE_PASSWORD_LOGIN = 1862
1919
ER_MUST_CHANGE_PASSWORD = 1820
20+
21+
EMPTY_PASSWORD_FLAG_SENTINEL = -1

mycli/main.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
DEFAULT_HOST,
5757
DEFAULT_PORT,
5858
DEFAULT_WIDTH,
59+
EMPTY_PASSWORD_FLAG_SENTINEL,
5960
ER_MUST_CHANGE_PASSWORD_LOGIN,
6061
ISSUES_URL,
6162
REPO_URL,
@@ -87,8 +88,6 @@
8788
sqlparse.engine.grouping.MAX_GROUPING_DEPTH = None # type: ignore[assignment]
8889
sqlparse.engine.grouping.MAX_GROUPING_TOKENS = None # type: ignore[assignment]
8990

90-
EMPTY_PASSWORD_FLAG_SENTINEL = -1
91-
9291

9392
class IntOrStringClickParamType(click.ParamType):
9493
name = 'text' # display as TEXT in helpdoc
@@ -1221,9 +1220,11 @@ class CliArgs:
12211220
'--password',
12221221
'password',
12231222
type=INT_OR_STRING_CLICK_TYPE,
1224-
is_flag=False,
1225-
flag_value=EMPTY_PASSWORD_FLAG_SENTINEL,
1226-
help='Prompt for (or pass in cleartext) the password to connect to the database.',
1223+
help=dedent(
1224+
"""Password to connect to the database.
1225+
Use with a value to set the password at the CLI, or alone in the last position to request a prompt.
1226+
"""
1227+
),
12271228
)
12281229
password_file: str | None = clickdc.option(
12291230
type=click.Path(),
@@ -1918,7 +1919,7 @@ def get_password_from_file(password_file: str | None) -> str | None:
19181919
def main() -> int | None:
19191920
try:
19201921
result = click_entrypoint.main(
1921-
filtered_sys_argv(),
1922+
filtered_sys_argv(), # type: ignore[arg-type]
19221923
standalone_mode=False, # disable builtin exception handling
19231924
prog_name='mycli',
19241925
)

mycli/packages/cli_utils.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
from __future__ import annotations
22

33
import sys
4+
from typing import Sequence
45

6+
from mycli.constants import EMPTY_PASSWORD_FLAG_SENTINEL
7+
8+
9+
def filtered_sys_argv() -> Sequence[str | int]:
10+
args: Sequence[str | int] = sys.argv[1:]
11+
password_flag_forms = ['-p', '--pass', '--password']
512

6-
def filtered_sys_argv() -> list[str]:
7-
args = sys.argv[1:]
813
if args == ['-h']:
914
args = ['--help']
10-
return args
15+
16+
if args and args[-1] in password_flag_forms:
17+
args = list(args) + [EMPTY_PASSWORD_FLAG_SENTINEL]
18+
19+
return list(args)
1120

1221

1322
def is_valid_connection_scheme(text: str) -> tuple[bool, str | None]:

test/pytests/test_cli_utils.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import pytest
44

5+
from mycli.constants import EMPTY_PASSWORD_FLAG_SENTINEL
56
from mycli.packages import cli_utils
67
from mycli.packages.cli_utils import (
78
filtered_sys_argv,
@@ -22,6 +23,13 @@ def test_filtered_sys_argv(monkeypatch, argv, expected):
2223
assert filtered_sys_argv() == expected
2324

2425

26+
@pytest.mark.parametrize('password_flag', ['-p', '--pass', '--password'])
27+
def test_filtered_sys_argv_appends_empty_password_sentinel(monkeypatch, password_flag):
28+
monkeypatch.setattr(cli_utils.sys, 'argv', ['mycli', 'database', password_flag])
29+
30+
assert filtered_sys_argv() == ['database', password_flag, EMPTY_PASSWORD_FLAG_SENTINEL]
31+
32+
2533
@pytest.mark.parametrize(
2634
('text', 'is_valid', 'invalid_scheme'),
2735
[

test/pytests/test_main.py

Lines changed: 1 addition & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
DEFAULT_USER,
2626
TEST_DATABASE,
2727
)
28-
from mycli.main import EMPTY_PASSWORD_FLAG_SENTINEL, MyCli, click_entrypoint
28+
from mycli.main import MyCli, click_entrypoint
2929
import mycli.main_modes.repl as repl_mode
3030
import mycli.packages.special
3131
from mycli.packages.special.main import COMMANDS as SPECIAL_COMMANDS
@@ -1218,64 +1218,6 @@ def run_query(self, query, new_line=True):
12181218
)
12191219

12201220

1221-
def test_password_flag_uses_sentinel(monkeypatch):
1222-
class Formatter:
1223-
format_name = None
1224-
1225-
class Logger:
1226-
def debug(self, *args, **args_dict):
1227-
pass
1228-
1229-
def warning(self, *args, **args_dict):
1230-
pass
1231-
1232-
class MockMyCli:
1233-
config = {
1234-
'main': {},
1235-
'alias_dsn': {},
1236-
'connection': {
1237-
'default_keepalive_ticks': 0,
1238-
},
1239-
}
1240-
1241-
def __init__(self, **_args):
1242-
self.logger = Logger()
1243-
self.destructive_warning = False
1244-
self.main_formatter = Formatter()
1245-
self.redirect_formatter = Formatter()
1246-
self.ssl_mode = 'auto'
1247-
self.my_cnf = {'client': {}, 'mysqld': {}}
1248-
self.default_keepalive_ticks = 0
1249-
1250-
def connect(self, **args):
1251-
MockMyCli.connect_args = args
1252-
1253-
def run_query(self, query, new_line=True):
1254-
pass
1255-
1256-
import mycli.main
1257-
1258-
monkeypatch.setattr(mycli.main, 'MyCli', MockMyCli)
1259-
runner = CliRunner()
1260-
1261-
result = runner.invoke(
1262-
mycli.main.click_entrypoint,
1263-
args=[
1264-
'--user',
1265-
'user',
1266-
'--host',
1267-
DEFAULT_HOST,
1268-
'--port',
1269-
f'{DEFAULT_PORT}',
1270-
'--database',
1271-
'database',
1272-
'--password',
1273-
],
1274-
)
1275-
assert result.exit_code == 0, result.output + ' ' + str(result.exception)
1276-
assert MockMyCli.connect_args['passwd'] == EMPTY_PASSWORD_FLAG_SENTINEL
1277-
1278-
12791221
def test_password_option_uses_cleartext_value(monkeypatch):
12801222
class Formatter:
12811223
format_name = None

0 commit comments

Comments
 (0)