I wrote a Python 3.5 application using the cmd module. The last thing I would like to implement is proper handling of the CTRL-C (sigint) signal. I would like it to behave more or less the way Bash does it:
Basically:
/test $ bla bla bla|
# user types CTRL-C
/test $ bla bla bla^C
/test $
Here is simplified code as a runnable sample:
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()
This almost works. When I press CTRL-C, ^C is printed at the cursor, but I still have to press enter. Then, the precmd
method notices its self._interrupted
flag set by the handler, and returns an empty line. This is as far as I could take it, but I would like to somehow not have to press that enter.
I guess I somehow need to force the input()
to return, does anybody have ideas?
I found some hacky ways to achieve the behavior you want with Ctrl-C.
use_rawinput=False
and replace stdin
This one sticks (more or less…) to the public interface of cmd.Cmd
. Unfortunately, it disables readline support.
You can set use_rawinput
to false and pass a different file-like object to replace stdin
in Cmd.__init__()
. In practice, only readline()
is called on this object. So you can create a wrapper for stdin
that catches the KeyboardInterrupt
and executes the behavior you want instead:
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()
When I run this on my terminal, Ctrl-C shows ^C
and switches to a new line.
input()
If you want the results of input()
, except you want different behavior for Ctrl-C, one way to do that would be to use a different function instead of 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'
However, if you just blindly set input = my_input
, you get infinite recursion because my_input()
will call input()
, which is now itself. But that's fixable, and you can patch the __builtins__
dictionary in the cmd
module to use your modified input()
method during Cmd.cmdloop()
:
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
# ...
Note that this changes input()
for all Cmd
objects, not just TestShell
objects. If this isn't acceptable to you, you could…
Cmd.cmdloop()
source and modify itSince you're subclassing it, you can make cmdloop()
do anything you want. "Anything you want" could include copying parts of Cmd.cmdloop()
and rewriting others. Either replace the call to input()
with a call to another function, or catch and handle KeyboardInterrupt
right there in your rewritten cmdloop()
.
If you're afraid of the underlying implementation changing with new versions of Python, you could copy the whole cmd
module into a new module, and change what you want.