在 Python cmd 模块中处理 CTRL-C

Handle CTRL-C in Python cmd module

我使用 cmd 模块编写了一个 Python 3.5 应用程序。我最不想实现的是正确处理 CTRL-C (sigint) 信号。我希望它的行为或多或少像 Bash 那样:

基本上:

/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 打印在光标处,但我仍然必须按 enter。然后,precmd 方法注意到它的 self._interrupted 标志由处理程序设置,并且 return 是一个空行。这是我所能接受的,但我想以某种方式不必按回车键。

我想我需要以某种方式强制 input() 到 return,有人有想法吗?

我发现了一些使用 Ctrl-C 来实现您想要的行为的 hacky 方法。

设置use_rawinput=False并替换stdin

这个(或多或少...)与 cmd.Cmd 的 public 界面保持一致。不幸的是,它禁用了 readline 支持。

您可以将 use_rawinput 设置为 false 并传递一个不同的类文件对象来替换 Cmd.__init__() 中的 stdin。实际上,只有 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(),现在它本身。但这是可以修复的,您可以在 cmd 模块中修补 __builtins__ 字典,以在 Cmd.cmdloop():

期间使用修改后的 input() 方法
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

    # ...

请注意,这会更改 所有 Cmd 个对象 input(),而不仅仅是 TestShell 个对象。如果您不能接受,您可以...

复制 Cmd.cmdloop() 源并修改它

因为你是它的子类,你可以让 cmdloop() 做任何你想做的事。 "Anything you want" 可能包括复制 Cmd.cmdloop() 的部分内容并重写其他内容。用对另一个函数的调用替换对 input() 的调用,或者在重写的 cmdloop().

中捕获并处理 KeyboardInterrupt

如果您担心底层实现随着 Python 的新版本而改变,您可以将整个 cmd 模块复制到一个新模块中,然后根据需要进行更改。