在 cmd.Cmd 命令行解释器中更好地处理 KeyboardInterrupt

Posted

技术标签:

【中文标题】在 cmd.Cmd 命令行解释器中更好地处理 KeyboardInterrupt【英文标题】:Better handling of KeyboardInterrupt in cmd.Cmd command line interpreter 【发布时间】:2012-02-07 10:56:32 【问题描述】:

在使用 python 的 cmd.Cmd 创建自定义 CLI 时,如何告诉处理程序中止当前行并给我一个新提示?

这是一个最小的例子:

# console_min.py
# run: 'python console_min.py'

import cmd, signal

class Console(cmd.Cmd):
    def __init__(self):
        cmd.Cmd.__init__(self)
        self.prompt = "[test] "
        signal.signal(signal.SIGINT, handler)

    def do_exit(self, args):
        return -1

    def do_EOF(self, args):
        return self.do_exit(args)

    def preloop(self):
        cmd.Cmd.preloop(self)
        self._hist    = []
        self._locals  = 
        self._globals = 

    def postloop(self):
        cmd.Cmd.postloop(self)
        print "Exiting ..."

    def precmd(self, line):
        self._hist += [ line.strip() ]
        return line

    def postcmd(self, stop, line):
        return stop

    def emptyline(self):
        return cmd.Cmd.emptyline(self)

    def handler(self, signum, frame):
        # self.emptyline() does not work here
        # return cmd.Cmd.emptyline(self) does not work here
        print "caught ctrl+c, press return to continue"

if __name__ == '__main__':
    console = Console()
    console.cmdloop()

非常感谢您的进一步帮助。

原始问题和更多详细信息: [目前以下建议已被整合到这个问题中——仍在寻找答案。已更新以修复错误。]

我已经尝试将处理程序移动到循环外的函数中,看看它是否更灵活,但似乎没有。

我正在使用 python 的cmd.Cmd module 创建我自己的命令行解释器来管理与某些软件的交互。我经常按 ctrl+c 期待流行的类似 shell 的行为,即返回一个新的提示而不对输入的任何命令进行操作。但是,它只是退出。我试图在代码中的各个点(预循环等)捕获 KeyboardInterrupt 异常,但无济于事。我在sigints 上读过一些文章,但不太清楚它如何适合这里。

更新:根据下面的建议,我尝试实现信号并且能够这样做,这样 ctrl+c 现在不会退出 CLI,但会打印消息。但是,我的新问题是我似乎无法告诉处理函数(见下文)在打印之外做很多事情。我希望 ctrl+c 基本上中止当前行并给我一个新的提示。

【问题讨论】:

【参考方案1】:

我目前正在使用 Cmd 模块创建一个 Shell。我遇到了同样的问题,我找到了解决方案。

代码如下:

class Shell(Cmd, object)
...
    def cmdloop(self, intro=None):
        print(self.intro)
        while True:
            try:
                super(Shell, self).cmdloop(intro="")
                break
            except KeyboardInterrupt:
                print("^C")
...

现在您在 shell 中有一个正确的 KeyboardInterrupt(又名 CTRL-C)处理程序。

【讨论】:

似乎正在收敛于一个解决方案,但我不知道如何将它集成到我自己的代码中(上图)。我必须弄清楚“超级”线。我需要在未来的某个时候尝试让它发挥作用。 在 Python 2.x 中,cmd.Cmd 是一个老式类,所以super 不起作用。见this question。 为什么一定要自己打电话给postloop() 或者自己打印intro,就此而言 intro 每次调用cmdloop() 时都会打印:如果您不希望每次 Ctrl-C 都再次打印介绍消息,则必须自己控制打印。【参考方案2】:

您可以只捕获cmd.Cmd.cmdloop() 引发的KeyboardInterrupt,而不是使用信号处理。您当然可以使用信号处理,但这不是必需的。

在 while 循环中运行对 cmdloop() 的调用,该循环会在出现 KeyboardInterrupt 异常时自行重新启动,但由于 EOF 而正常终止。

import cmd
import sys

class Console(cmd.Cmd):
    def do_EOF(self,line):
        return True
    def do_foo(self,line):
        print "In foo"
    def do_bar(self,line):
        print "In bar"
    def cmdloop_with_keyboard_interrupt(self):
        doQuit = False
        while doQuit != True:
            try:
                self.cmdloop()
                doQuit = True
            except KeyboardInterrupt:
                sys.stdout.write('\n')

console = Console()

console.cmdloop_with_keyboard_interrupt()

print 'Done!'

执行 CTRL-c 只会在新行上打印一个新提示。

(Cmd) help

Undocumented commands:
======================
EOF  bar  foo  help

(Cmd) <----- ctrl-c pressed
(Cmd) <------ctrl-c pressed 
(Cmd) ddasfjdfaslkdsafjkasdfjklsadfljk <---- ctrl-c pressed
(Cmd) 
(Cmd) bar
In bar
(Cmd) ^DDone!

【讨论】:

这是正确的解决方案。如果KeyboardInterrupt 被捕获,它可以将self.intro 设置为None 可以将while doQuit 循环放在__init__() 的末尾,而不是创建cmdloop_with_keyboard_interrupt() 方法,然后控制台将在调用Console() 时运行。 cmdloop() 成功完成后使用while True:break 而不是doQuit 会更简单一些。【参考方案3】:

您可以使用signal handler 捕获CTRL-C signal。如果你添加下面的代码,解释器拒绝在 CTRL-C 上退出:

import signal

def handler(signum, frame):
    print 'Caught CTRL-C, press enter to continue'

signal.signal(signal.SIGINT, handler)

如果您不想在每次 CTRL-C 后按 ENTER,只需让处理程序什么都不做,这将捕获信号而没有任何效果:

def handler(signum, frame):
    """ just do nothing """

【讨论】:

什么都不做可以在Python中使用pass来表达。【参考方案4】:

回应this answer中的以下评论:

这似乎是一个解决方案,但我不知道如何将它集成到我自己的代码中(上图)。我必须弄清楚“超级”线。我需要在未来的某个时候尝试让它发挥作用。

如果除了cmd.Cmd 之外,您的课程还扩展objectsuper() 将在此答案中起作用。像这样:

class Console(cmd.Cmd, object):

【讨论】:

【参考方案5】:

我也想做同样的事情,所以我搜索了它并创建了基本脚本以供理解,我的代码可以在我的GitHub上找到

所以,在你的代码中,

而不是这个

if __name__ == '__main__':
    console = Console()
    console.cmdloop()

用这个,

if __name__ == '__main__':
    console = Console()
    try: 
       console.cmdloop()
    except KeyboardInterrupt:
       print ("print sth. ")
       raise SystemExit

希望这对您有所帮助。好吧,它对我有用。 ?

【讨论】:

【参考方案6】:

您可以查看signal 模块:http://docs.python.org/library/signal.html

import signal

oldSignal = None

def handler(signum, frame):
    global oldSignal
    if signum == 2:
        print "ctrl+c!!!!"
    else:
        oldSignal()

oldSignal = signal.signal(signal.SIGINT, handler)

【讨论】:

【参考方案7】:

只需在 Console(cmd.Cmd) 类中添加这个:


    def cmdloop(self):
        try:
            cmd.Cmd.cmdloop(self)
        except KeyboardInterrupt as e:
            self.cmdloop()

忘记所有其他的东西。这行得通。它不会在循环内产生循环。当它捕获KeyboardInterrupt 时,它会调用do_EOF,但只会执行第一行;因为您在do_EOF 中的第一行是返回do_exit,这很好。do_exit 调用postloop。 然而,同样,它只执行cmd.Cmd.postloop(self) 之后的第一行。在我的程序中,这是打印“\n”。奇怪的是,如果你是垃圾邮件 ctrl+C,你最终会看到它打印第二行,通常只在实际退出时打印(ctrl+Z 然后输入,或输入退出)。

【讨论】:

这是我最终采用的方法。唯一可能的问题是,如果“intro”是我上面示例中设置的属性,它将不断打印介绍(灵感来自下面的@seb 评论)。所以我希望可以编辑您的评论以反映这一点。 递归限制有人吗?如果两个KeyboardInterrupts 靠得太近也会有问题。【参考方案8】:

我更喜欢信号方法,但只是通过。并不真正关心在我的 shell 环境中提示用户任何内容。

import signal

def handler(signum, frame):
    pass

signal.signal(signal.SIGINT, handler)

【讨论】:

这根本不符合 OP 的要求 - 即忽略当前在提示中写入的输入。

以上是关于在 cmd.Cmd 命令行解释器中更好地处理 KeyboardInterrupt的主要内容,如果未能解决你的问题,请参考以下文章

CMD是啥意思?

CMD是计算机操作系统吗?Linux有没有CMD命令

计算机中的cmd是啥意思,全称是啥

Python-模块-cmd

设置Windows系统的cmd命令行终端的代理

CMD的最佳“代替品”