2016-05-22 13 views
5

Ich schrieb eine Python 3.5-Anwendung mit dem Cmd-Modul. Das letzte, was ich implementieren möchte, ist die richtige Handhabung des CTRL-C (Sigint) -Signals. Ich mag es mehr oder weniger die Art und Weise Bash verhalten tut es:Handle CTRL-C in Python-Cmd-Modul

  • Druck^C dem Punkt, an dem Cursor
  • klar den Puffer ist, so dass die Eingabe von Text
  • Springen zum nächsten gelöscht Druckzeile, die Eingabeaufforderung, und warten Eingangs

Grundsätzlich gilt:

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

hier Code als runnable vereinfacht Beispiel:

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

Das funktioniert fast. Wenn ich CTRL-C drücke, wird^C am Cursor gedruckt, aber ich muss noch die Eingabetaste drücken. Dann bemerkt die precmd-Methode ihr self._interrupted-Flag, das von dem Handler festgelegt wird, und gibt eine leere Zeile zurück. Das ist soweit ich es könnte, aber ich möchte irgendwie das nicht eingeben müssen.

Ich denke, ich muss irgendwie zwingen, die input() zurückzukehren, hat jemand Ideen?

+0

Es würde helfen, eine kleine ausführbare Probe zu haben. Ohne Ihren Code zu schreiben, wie Sie gerade vorgehen, würde es ausreichen, ein '' \ n'' oder ein Leerzeichen nach 'stdout' zu schreiben? Das heißt, sobald der Signal-Handler ausgelöst wurde, wird am Ende von ''^C'' ein Zeilenumbruch angefügt. – tijko

+1

@tijko Bearbeiten Sie den Beitrag, indem Sie das Snippet durch ein ausführbares Beispiel ersetzen. Ich habe versucht, eine neue Zeile zu schreiben, um stdout (es ist eigentlich standardmäßig durch die Druckmethode gemacht), aber es funktioniert nicht, und warum sollte es? Es würde erfordern, dass stdin von stdout gefüttert wird, was nicht der Fall ist, es ist nicht piped. – wujek

+0

Yeah, du hast Recht, ich habe nicht in Bezug auf Befehlszeile stdin Prompt gedacht. @ DanGetz hat eine Antwort, die Ihren Bedürfnissen entspricht, glaube ich. Es ist auch sauberer, keine Notwendigkeit, Signalhandler zu setzen oder Flags zu setzen. Nach deinem Kommentar habe ich begonnen, entlang der Linien der Umleitung von stdin zu denken, du kannst dich anpassen, wie auch immer du es brauchst. – tijko

Antwort

3

Ich habe einige hacky Wege gefunden, um das gewünschte Verhalten mit Ctrl-C zu erreichen.

Set use_rawinput=False und stdin

diesen einen Sticks (mehr oder weniger ...) an die öffentliche Schnittstelle cmd.Cmd ersetzen. Leider deaktiviert es die Readline-Unterstützung.

Sie können use_rawinput auf false setzen und ein anderes dateiähnliches Objekt übergeben, um stdin in Cmd.__init__() zu ersetzen. In der Praxis wird für dieses Objekt nur readline() aufgerufen. So können Sie einen Wrapper für stdin erstellen, die die KeyboardInterrupt fängt und führt das Verhalten, das Sie wollen stattdessen:

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

Als ich laufen diese auf meinem Terminal, Ctrl-C zeigt ^C und schaltet auf eine neue Zeile.

Monkey-Patch input()

Wenn Sie die Ergebnisse von input() wollen, außer dass Sie ein anderes Verhalten für Ctrl-C, eine Möglichkeit, das zu tun wollen eine andere Funktion statt input() zu verwenden wäre:

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' 

Wenn Sie jedoch blind input = my_input setzen, erhalten Sie unendliche Rekursion, weil my_input()input() aufrufen wird, die jetzt selbst ist.Aber das ist fixierbar, und Sie können in der cmd Modul zu benutzen, um Ihre modifizierte input() Methode während Cmd.cmdloop() das __builtins__ Wörterbuch Patch:

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 

    # ... 

Beachten Sie, dass sich dies ändert input() für alle Cmd Objekte, nicht nur TestShell Objekte. Wenn Ihnen das nicht akzeptabel ist, könnten Sie ...

Kopieren Sie die Cmd.cmdloop() Quelle und ändern Sie es

Da Sie es sind Subklassen, können Sie cmdloop() etwas machen wollen Sie. "Alles, was Sie wollen" könnte Teile von Cmd.cmdloop() kopieren und andere neu schreiben. Ersetzen Sie den Anruf entweder durch input() durch einen Anruf an eine andere Funktion, oder fangen Sie und behandeln Sie KeyboardInterrupt direkt dort in Ihrem neu geschriebenen cmdloop().

Wenn Sie befürchten, dass sich die zugrunde liegende Implementierung mit neuen Versionen von Python ändert, können Sie das gesamte cmd-Modul in ein neues Modul kopieren und ändern, was Sie wollen.

+0

Danke für Ihre Lösung. Leider benutze ich readline und es ist ziemlich wichtig. – wujek

+0

@wujek Ja, das habe ich erwartet. Nun, es gibt einen etwas hackeren Weg damit umzugehen, denke ich, ich füge es hinzu. –

+0

Ich nahm Option 3, kopierte einfach den cmdloop() Code und änderte. Ich denke, es ist die am wenigsten hacky Lösung ... Danke! – wujek