15

While using python's cmd.Cmd to create a custom CLI, how do I tell the handler to abort the current line and give me a new prompt?

Here is a minimal example:

# 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()

Further help is greatly appreciated.

Original Question and more details: [Currently the suggestions below have been integrated into this question -- still searching for an answer. Updated to fix an error.]

I've since tried moving the handler to a function outside the loop to see if it were more flexible, but it does not appear to be.

I am using python's cmd.Cmd module to create my own command line interpreter to manage interaction with some software. I often press ctrl+c expecting popular shell-like behavior of returning a new prompt without acting on whatever command was typed. However, it just exits. I've tried to catch KeyboardInterrupt exceptions at various points in the code (preloop, etc.) to no avail. I've read a bit on sigints but don't quite know how that fits in here.

UPDATE: From suggestions below, I tried to implement signal and was able to do so such that ctrl+c now doesn't exit the CLI but does print the message. However, my new issue is that I cannot seem to tell the handler function (see below) to do much beside print. I would like ctrl+c to basically abort the current line and give me a new prompt.

8 Answers 8

16

I'm currently working on a creation of a Shell by using the Cmd module. I've been confronted with the same issue, and I found a solution.

Here is the code:

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")
...

Now you have a proper KeyboardInterrupt (aka CTRL-C) handler within the shell.

Sign up to request clarification or add additional context in comments.

8 Comments

This appears to be converging on a solution, but I don't know how to integrate it into my own code (above). I have to figure out the 'super' line. I need to try and get this working at some point in the future.
In Python 2.x, cmd.Cmd is an old-style class, so super won't work. See this question.
Why do you have to call postloop() yourself?
Or print intro yourself, for that matter
intro is printed every time cmdloop() is called: if you do not want the introduction message to be printed again for every Ctrl-C, you have to control the printing yourself.
|
7

Instead of using signal handling you could just catch the KeyboardInterrupt that cmd.Cmd.cmdloop() raises. You can certainly use signal handling but it isn't required.

Run the call to cmdloop() in a while loop that restarts itself on an KeyboardInterrupt exception but terminates properly due to 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!'

Doing a CTRL-c just prints a new prompt on a new line.

(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!

3 Comments

This is the proper solution. It could set self.intro to None if KeyboardInterrupt was caught.
Instead of creating a cmdloop_with_keyboard_interrupt() method, the while doQuit loop can be placed at the end of an __init__() and then the console will just run when calling Console().
Instead of the doQuit thing, it would be a bit simpler to use while True: and then break after cmdloop() successfully completes.
1

You can catch the CTRL-C signal with a signal handler. If you add the code below, the interpreter refuses to quit on CTRL-C:

import signal

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

signal.signal(signal.SIGINT, handler)

If you don't want to press ENTER after each CTRL-C, just let the handler do nothing, which will trap the signal without any effect:

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

1 Comment

Just do nothing can be expressed using pass in Python.
1

I wanted to do the same, so I searched for it and created basic script for understanding purposes, my code can be found on my GitHub

So, In your code,

instead of this

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

Use this,

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

Hope this will help you out. well, it worked for me. 😀

Comments

0

You can check out the signal module: 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)

Comments

0

In response to the following comment in this answer:

This appears to be converging on a solution, but I don't know how to integrate it into my own code (above). I have to figure out the 'super' line. I need to try and get this working at some point in the future.

super() would work in this answer if you have your class extend object in addition to cmd.Cmd. Like this:

class Console(cmd.Cmd, object):

Comments

-2

Just add this inside the class Console(cmd.Cmd):


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

Forget all the other stuff. This works. It doesn't make loops inside of loops. As it catches KeyboardInterrupt it calls do_EOF but only will execute the first line; since your first line in do_EOF is return do_exit this is fine.
do_exit calls postloop.
However, again, it only executes the first line after cmd.Cmd.postloop(self). In my program this is print "\n". Strangely, if you SPAM ctrl+C you will eventually see it print the 2nd line usually only printed on ACTUAL exit (ctrl+Z then enter, or typing in exit).

2 Comments

This was the approach I ended up taking. The only possible issue is that if 'intro' were to be an attribute set in my example above, it would continually print the intro (inspired from @seb's comment below). So I hope it's okay to edit your comment to reflect that.
Recursion limit anyone? There's also a problem if two KeyboardInterrupts are too close.
-2

I prefer the signal method, but with just pass. Don't really care about prompting the user with anything in my shell environment.

import signal

def handler(signum, frame):
    pass

signal.signal(signal.SIGINT, handler)

1 Comment

This doesn't do what OP wants at all - namely, to ignore the input that was currently written in the prompt.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.