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

Я написал приложение 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() вернуться, у кого-нибудь есть идеи?


person wujek    schedule 22.05.2016    source источник
comment
Было бы полезно иметь небольшой рабочий образец. Без запуска вашего кода, как вы это делаете в настоящее время, будет ли достаточно написать '\n' или пробел в stdout? То есть после того, как обработчик сигнала отключится, добавьте новую строку в конце '^C'   -  person tijko    schedule 22.05.2016
comment
@tijko Отредактировал сообщение, заменив фрагмент исполняемым образцом. Я пытался написать новую строку в stdout (на самом деле это делается по умолчанию методом печати), но это не работает, да и зачем? Для stdin потребуется подача из stdout, что не так, это не конвейер.   -  person wujek    schedule 22.05.2016
comment
Да, вы правы, я не думал о командной строке stdin. Думаю, у @DanGetz есть ответ, который удовлетворит ваши потребности. Это также чище, нет необходимости устанавливать обработчик сигналов или устанавливать флаги. После вашего комментария я начал думать о перенаправлении стандартного ввода, вы можете настроить его так, как вам нужно.   -  person tijko    schedule 23.05.2016


Ответы (1)


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

Установите use_rawinput=False и замените stdin

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

Вы можете установить для use_rawinput значение false и передать другой файловый объект для замены 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 в новый модуль и изменить то, что хотите.

person Dan Getz    schedule 22.05.2016
comment
Спасибо за ваше решение. К сожалению, я использую readline, и это очень важно. - person wujek; 23.05.2016
comment
@wujek да, я ожидал этого. Что ж, думаю, есть немного более хакерский способ справиться с этим, я добавлю его. - person Dan Getz; 23.05.2016
comment
Я выбрал вариант 3, просто скопировал код cmdloop() и изменил. Я думаю, что это наименее хакерское решение... Спасибо! - person wujek; 23.05.2016