Обрабатывать CTRL-C в модуле Python cmd

Я написал приложение Python 3.5, используя модуль cmd. Последнее, что я хотел бы реализовать, - это правильная обработка сигнала CTRL-C (sigint). Я бы хотел, чтобы он вел себя более или менее так, как это делает Bash:

  • выведите ^C в том месте, где находится курсор
  • очистить буфер так, чтобы входной текст был удален
  • перейдите к следующей строке, распечатайте подсказку и дождитесь ввода

В принципе:

/test $ bla bla bla|
# user types CTRL-C
/test $ bla bla bla^C
/test $ 

Вот упрощенный код в качестве запускаемого примера:

import cmd
import signal


class TestShell(cmd.Cmd):
    def __init__(self):
        super().__init__()

        self.prompt = '$ '

        signal.signal(signal.SIGINT, handler=self._ctrl_c_handler)
        self._interrupted = False

    def _ctrl_c_handler(self, signal, frame):
        print('^C')
        self._interrupted = True

    def precmd(self, line):
        if self._interrupted:
            self._interrupted = False
            return ''

        if line == 'EOF':
            return 'exit'

        return line

    def emptyline(self):
        pass

    def do_exit(self, line):
        return True


TestShell().cmdloop()

Это почти работает. Когда я нажимаю CTRL-C, на курсоре печатается ^C, но мне все равно приходится нажимать ввод. Затем precmd метод замечает его self._interrupted флаг, установленный обработчиком, и возвращает пустую строку. Это, насколько я мог понять, но я хотел бы как-то не нажимать, чтобы войти.

Я думаю, мне как-то нужно заставить input() вернуться, у кого-нибудь есть идеи?

1 ответ

Решение

Я нашел несколько хакерских способов добиться желаемого поведения с помощью Ctrl-C.

Задавать use_rawinput=False и заменить stdin

Этот придерживается (более или менее...) к общедоступному интерфейсу cmd.Cmd, К сожалению, это отключает поддержку readline.

Вы можете установить use_rawinput ложь и передать другой файлоподобный объект для замены stdin в Cmd.__init__(), На практике только readline() вызывается на этом объекте. Таким образом, вы можете создать оболочку для stdin что ловит KeyboardInterrupt и выполняет поведение, которое вы хотите вместо этого:

class _Wrapper:

    def __init__(self, fd):
        self.fd = fd

    def readline(self, *args):
        try:
            return self.fd.readline(*args)
        except KeyboardInterrupt:
            print()
            return '\n'


class TestShell(cmd.Cmd):

    def __init__(self):
        super().__init__(stdin=_Wrapper(sys.stdin))
        self.use_rawinput = False
        self.prompt = '$ '

    def precmd(self, line):
        if line == 'EOF':
            return 'exit'
        return line

    def emptyline(self):
        pass

    def do_exit(self, line):
        return True


TestShell().cmdloop()

Когда я запускаю это на своем терминале, Ctrl-C показывает ^C и переключается на новую линию.

Обезьяна-патч input()

Если вы хотите результаты input(), за исключением того, что вы хотите другое поведение для Ctrl-C, один из способов сделать это будет использовать другую функцию вместо input():

def my_input(*args):   # input() takes either no args or one non-keyword arg
    try:
        return input(*args)
    except KeyboardInterrupt:
        print('^C')   # on my system, input() doesn't show the ^C
        return '\n'

Тем не менее, если вы просто вслепую input = my_input Вы получаете бесконечную рекурсию, потому что my_input() позвоню input(), который сейчас сам. Но это поправимо, и вы можете исправить __builtins__ словарь в cmd модуль для использования вашего модифицированного input() метод во время Cmd.cmdloop():

def input_swallowing_interrupt(_input):
    def _input_swallowing_interrupt(*args):
        try:
            return _input(*args)
        except KeyboardInterrupt:
            print('^C')
            return '\n'
    return _input_swallowing_interrupt


class TestShell(cmd.Cmd):

    def cmdloop(self, *args, **kwargs):
        old_input_fn = cmd.__builtins__['input']
        cmd.__builtins__['input'] = input_swallowing_interrupt(old_input_fn)
        try:
            super().cmdloop(*args, **kwargs)
        finally:
            cmd.__builtins__['input'] = old_input_fn

    # ...

Обратите внимание, что это меняется input() для всех Cmd объекты, а не только TestShell объекты. Если это не приемлемо для вас, вы могли бы...

Скопируйте Cmd.cmdloop() источник и изменить его

Так как вы подклассы его, вы можете сделать cmdloop() делай что хочешь. "Все, что вы хотите" может включать в себя копирование частей Cmd.cmdloop() и переписывать другие. Либо заменить вызов input() с вызовом другой функции, или поймать и обработать KeyboardInterrupt прямо в вашей переписанной cmdloop(),

Если вы боитесь изменения базовой реализации с новыми версиями Python, вы можете скопировать всю cmd модуль в новый модуль, и измените то, что вы хотите.

Другие вопросы по тегам