13

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.

Community
  • 1
  • 1
rocking_ellipse
  • 275
  • 1
  • 2
  • 13

8 Answers8

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.

Eric O. Lebigot
  • 91,433
  • 48
  • 218
  • 260
Seb
  • 184
  • 1
  • 4
  • 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. – rocking_ellipse Dec 05 '12 at 16:31
  • In Python 2.x, `cmd.Cmd` is an *old-style class*, so `super` won't work. See [this question](http://stackoverflow.com/questions/770134). – Jonathon Reinhart Dec 11 '15 at 18:19
  • 2
    Why do you have to call `postloop()` yourself? – Jonathon Reinhart Dec 11 '15 at 18:37
  • Or print `intro` yourself, for that matter – Eric Jan 06 '17 at 10:22
  • `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. – Eric O. Lebigot May 21 '18 at 10:21
  • Now, `postloop()` is called when the `cmdloop()` exits normally, so indeed it should not be called (again) in the solution. I'm removing it from the answer. – Eric O. Lebigot May 21 '18 at 10:23
  • `cmd.Cmd` being an old-style class is handled by `Shell` inheriting from `object` itself! See https://stackoverflow.com/a/36985714/42973. – Eric O. Lebigot May 21 '18 at 10:28
  • With this solution also the `preloop` hook will be called every time you hit CTRL+C. Remember to deregister it once called the first time – mrnfrancesco Sep 13 '19 at 15:11
6

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!
Joel
  • 2,928
  • 2
  • 24
  • 34
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):
Community
  • 1
  • 1
deargle
  • 487
  • 4
  • 8
0

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.

1uffyD9
  • 61
  • 1
  • 3
0

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 """
miku
  • 181,842
  • 47
  • 306
  • 310
-1

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)
jdi
  • 90,542
  • 19
  • 167
  • 203
-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).

Freakyuser
  • 2,774
  • 17
  • 45
  • 72
  • 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. – rocking_ellipse Jun 10 '14 at 19:08
  • 5
    Recursion limit anyone? There's also a problem if two `KeyboardInterrupt`s are too close. – Veedrac Jun 10 '14 at 19:15
-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)
Jonathon Reinhart
  • 132,704
  • 33
  • 254
  • 328