diff --git a/osc/commandline.py b/osc/commandline.py index 68374ea0..00bd0aec 100644 --- a/osc/commandline.py +++ b/osc/commandline.py @@ -36,6 +36,7 @@ from . import store as osc_store from .core import * from .grabber import OscFileGrabber from .meter import create_text_meter +from .output import get_user_input from .util import cpio, rpmquery, safewriter from .util.helper import _html_escape, format_table @@ -7307,11 +7308,13 @@ Please submit there instead, or use --nodevelproject to force direct submission. build_root = osc_build.calculate_build_root(apihost, prj, pac, repo, arch, user) if opts.wipe and not opts.force: # Confirm delete - print(f"Really wipe '{build_root}'? [y/N]: ", end="") - choice = raw_input().lower() - if choice != 'y': - print('Aborting') - sys.exit(0) + reply = get_user_input( + f"Really wipe '{build_root}'?", + answers={"y": "yes", "n": "no"}, + default_answer="n", + ) + if reply != "y": + raise oscerr.UserAbort() build_args = ['--root=' + build_root, '--noinit', '--shell'] if opts.wipe: build_args.append('--wipe') diff --git a/osc/output/__init__.py b/osc/output/__init__.py index e3129166..65665c7d 100644 --- a/osc/output/__init__.py +++ b/osc/output/__init__.py @@ -1,4 +1,5 @@ from .key_value_table import KeyValueTable +from .input import get_user_input from .tty import colorize from .widechar import wc_ljust from .widechar import wc_width diff --git a/osc/output/input.py b/osc/output/input.py new file mode 100644 index 00000000..ba42a181 --- /dev/null +++ b/osc/output/input.py @@ -0,0 +1,51 @@ +import sys +import textwrap +from typing import Dict +from typing import Optional + +from .. import oscerr +from .tty import colorize + + +def get_user_input(question: str, answers: Dict[str, str], default_answer: Optional[str] = None) -> str: + """ + Ask user a question and wait for reply. + + :param question: The question. The text gets automatically dedented and stripped. + :param answers: A dictionary with answers. Keys are the expected replies and values are their descriptions. + :param default_answer: The default answer. Must be ``None`` or match an ``answers`` entry. + """ + + if default_answer and default_answer not in answers: + raise ValueError(f"Default answer doesn't match any answer: {default_answer}") + + question = textwrap.dedent(question) + question = question.strip() + + prompt = [] + for key, value in answers.items(): + value = f"{colorize(key, 'bold')}){value}" + prompt.append(value) + + prompt_str = " / ".join(prompt) + if default_answer: + prompt_str += f" (default={colorize(default_answer, 'bold')})" + prompt_str += ": " + + print(question, file=sys.stderr) + + while True: + try: + reply = input(prompt_str) + except EOFError: + # interpret ctrl-d as user abort + raise oscerr.UserAbort() # pylint: disable=raise-missing-from + + if reply in answers: + return reply + if reply.strip() in answers: + return reply.strip() + if not reply.strip(): + return default_answer + + print(f"Invalid reply: {colorize(reply, 'bold,red')}", file=sys.stderr)