Handle CTRL-C in cmd module in Python

I wrote a Python 3.5 application using the cmd module. The last thing I would like to implement is the proper processing of the CTRL-C signal (sigint). I would like it to behave more or less like Bash does:

  • print ^ C at the point where the cursor is
  • clear the buffer so that the input text is deleted
  • go to the next line, print the invitation and wait for input

Basically:

/test $ bla bla bla| # user types CTRL-C /test $ bla bla bla^C /test $ 

Here is a simplified code as an executable 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() 

It almost works. When I press CTRL-C, ^ C is printed by the cursor, but I still need to press enter. The precmd method precmd notices the self._interrupted flag set by the handler and returns an empty string. This is as far as I can understand it, but I would like to somehow not click to enter.

I think for some reason I need to get input() to return, does anyone have any ideas?

+5
source share
1 answer

I found some hacker ways to achieve the desired behavior using Ctrl-C.

Set use_rawinput=False and replace stdin

This file attaches (more or less ...) to the common cmd.Cmd interface. Unfortunately, this disables readline support.

You can set use_rawinput to false and pass another file-like object to replace stdin in Cmd.__init__() . In practice, only readline() is called for this object. Thus, you can create a wrapper for stdin that catches KeyboardInterrupt and performs the required behavior:

 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 displays ^C and switches to a new line.

Monkey-patch input()

If you want input() results, except that you want different behavior for Ctrl-C, one way to do this 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 will get infinite recursion because my_input() will call input() , which is now itself. But this is a fix, and you can fix 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 modifies input() for all cmd objects , not just TestShell objects. If this is unacceptable to you, you can ...

Copy the source of Cmd.cmdloop() and change it

Since you subclass it, you can make cmdloop() do anything. "Everything you want" may include copying parts of Cmd.cmdloop() and rewriting others. Either replace the call with input() calling another function, or catch and process KeyboardInterrupt right there in your rewritten cmdloop() .

If you are afraid that the base implementation will change with new versions of Python, you can copy the entire cmd module into a new module and change what you want.

+3
source

All Articles