Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ prompt is displayed.
- **get_rprompt**: override to populate right prompt
- **pre_prompt**: hook method that is called before the prompt is displayed, but after
`prompt-toolkit` event loop has started
- **read_secret**: read secrets like passwords without displaying them to the terminal
- New settables:
- **max_column_completion_results**: (int) the maximum number of completion results to
display in a single column
Expand Down
18 changes: 18 additions & 0 deletions cmd2/cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -3391,6 +3391,24 @@ def read_input(

return self._read_raw_input(prompt, temp_session)

def read_secret(
self,
prompt: str = '',
) -> str:
"""Read a secret from stdin without displaying the value on the screen.

:param prompt: prompt to display to user
:return: the secret read from stdin with all trailing new lines removed
:raises EOFError: if the input stream is closed or the user signals EOF (e.g., Ctrl+D)
:raises Exception: any other exceptions raised by prompt()
"""
temp_session: PromptSession[str] = PromptSession(
input=self.main_session.input,
output=self.main_session.output,
)

return self._read_raw_input(prompt, temp_session, is_password=True)

def _process_alerts(self) -> None:
"""Background worker that processes queued alerts and dynamic prompt updates."""
while True:
Expand Down
4 changes: 2 additions & 2 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ each:
- Shows how cmd2's built-in `run_pyscript` command can provide advanced Python scripting of cmd2
applications
- [read_input.py](https://github.com/python-cmd2/cmd2/blob/main/examples/read_input.py)
- Demonstrates the various ways to call `cmd2.Cmd.read_input()` for input history and tab
completion
- Demonstrates the various ways to call `cmd2.Cmd.read_input()` and `cmd2.Cmd.read_secret()` for
input history, tab completion, and password masking
- [remove_builtin_commands.py](https://github.com/python-cmd2/cmd2/blob/main/examples/remove_builtin_commands.py)
- Shows how to remove any built-in cmd2 commands you do not want to be present in your cmd2
application
Expand Down
16 changes: 15 additions & 1 deletion examples/read_input.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env python
"""A simple example demonstrating the various ways to call cmd2.Cmd.read_input() for input history and tab completion.
"""A simple example demonstrating the various ways to call cmd2.Cmd.read_input() and cmd2.Cmd.read_secret().

These methods can be used to read input from stdin with optional history, tab completion, or password masking.
It also demonstrates how to use the cmd2.Cmd.select method.
"""

Expand Down Expand Up @@ -97,6 +98,19 @@ def do_custom_parser(self, _) -> None:
else:
self.custom_history.append(input_str)

@cmd2.with_category(EXAMPLE_COMMANDS)
def do_read_password(self, _) -> None:
"""Call read_secret to read a password without displaying it while being typed.

WARNING: Password will be displayed for verification after it is typed.
"""
self.poutput("The input will not be displayed on the screen")
try:
password = self.read_secret("Password: ")
self.poutput(f"You entered: {password}")
except EOFError:
pass

def do_eat(self, arg):
"""Example of using the select method for reading multiple choice input.

Expand Down
23 changes: 23 additions & 0 deletions tests/test_cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -2236,6 +2236,29 @@ def test_read_input_eof(base_app, monkeypatch) -> None:
base_app.read_input("Prompt> ")


def test_read_secret(base_app, monkeypatch):
"""Test read_secret passes is_password=True to _read_raw_input."""
with mock.patch.object(base_app, '_read_raw_input') as mock_reader:
mock_reader.return_value = "my_secret"

secret = base_app.read_secret("Secret: ")

assert secret == "my_secret"
# Verify it called _read_raw_input with is_password=True
args, kwargs = mock_reader.call_args
assert args[0] == "Secret: "
assert kwargs['is_password'] is True


def test_read_secret_eof(base_app, monkeypatch):
"""Test that read_secret passes up EOFErrors."""
read_raw_mock = mock.MagicMock(name='_read_raw_input', side_effect=EOFError)
monkeypatch.setattr("cmd2.Cmd._read_raw_input", read_raw_mock)

with pytest.raises(EOFError):
base_app.read_secret("Secret: ")


def test_read_input_passes_all_arguments_to_resolver(base_app):
mock_choices = ["choice1", "choice2"]
mock_provider = mock.MagicMock(name="provider")
Expand Down
Loading