rocking_ellipse
rocking_ellipse

Reputation: 295

Better handling of KeyboardInterrupt in cmd.Cmd command line interpreter

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.

Upvotes: 15

Views: 11451

Answers (8)

1uffyD9
1uffyD9

Reputation: 81

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. 😀

Upvotes: 1

Seb
Seb

Reputation: 184

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.

Upvotes: 16

deargle
deargle

Reputation: 517

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):

Upvotes: 0

Adam Cooley
Adam Cooley

Reputation: 17

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)

Upvotes: -2

Abei Villafane
Abei Villafane

Reputation: 83

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

Upvotes: -2

Joel
Joel

Reputation: 2966

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!

Upvotes: 7

miku
miku

Reputation: 188004

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

Upvotes: 1

jdi
jdi

Reputation: 92559

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)

Upvotes: 0

Related Questions