Обрабатывать 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
модуль в новый модуль, и измените то, что вы хотите.