代码之家  ›  专栏  ›  技术社区  ›  Jason

python从外部启动编辑器并获取文本

  •  0
  • Jason  · 技术社区  · 7 年前

    我正在编写一个PyQt程序,允许用户启动他们喜欢的编辑器来填充TextEdit字段。

    我发现了一些类似的问题 Opening vi from Python call up an EDITOR (vim) from a python script , invoke an editor ( vim ) in python git commit 命令。我所追求的是一种“非阻塞”方式(因为它是一个GUI),类似于中的“编辑源代码”功能 zimwiki

    我当前的尝试:

    import os
    import tempfile
    import threading
    import subprocess
    
    def popenAndCall(onExit, popenArgs):
    
        def runInThread(onExit, popenArgs):
            tmppath=popenArgs[-1]
            proc = subprocess.Popen(popenArgs)
            # this immediately finishes OPENING vim.
            rec=proc.wait()
            print('# <runInThread>: rec=', rec)
            onExit(tmppath)
            os.remove(tmppath)
            return
    
        thread = threading.Thread(target=runInThread, args=(onExit, popenArgs))
        thread.start()
        return thread
    
    def openEditor():
    
        fd, filepath=tempfile.mkstemp()
        print('filepath=',filepath)
    
        def cb(tmppath):
            print('# <cb>: cb tmppath=',tmppath)
            with open(tmppath, 'r') as tmp:
                lines=tmp.readlines()
                for ii in lines:
                    print('# <cb>: ii',ii)
            return
    
        with os.fdopen(fd, 'w') as tmp:
    
            cmdflag='--'
            editor_cmd='vim'
            cmd=[os.environ['TERMCMD'], cmdflag, editor_cmd, filepath]
            print('#cmd = ',cmd)
    
            popenAndCall(cb, cmd)
            print('done')
    
        return
    
    
    if __name__=='__main__':
    
        openEditor()
    

    Popen.wait() 只等待编辑器打开,而不是关闭编辑器。所以它没有从编辑器中捕获任何内容。

    你知道怎么解决这个问题吗?谢谢!

    编辑:

    answer os 等待 process group ,但还是不起作用。代码如下:

    def popenAndCall(onExit, popenArgs):
    
        def runInThread(onExit, popenArgs):
            tmppath=popenArgs[-1]
            proc = subprocess.Popen(popenArgs, preexec_fn=os.setsid)
            pid=proc.pid
            gid=os.getpgid(pid)
            #rec=proc.wait()
            rec=os.waitid(os.P_PGID, gid, os.WEXITED | os.WSTOPPED)
            print('# <runInThread>: rec=', rec, 'pid=',pid, 'gid=',gid)
    
            onExit(tmppath)
            os.remove(tmppath)
            return
    
        thread = threading.Thread(target=runInThread, args=(onExit, popenArgs))
        thread.start()
        return thread
    

    我想是这样 gid=os.getpgid(pid) 给我组的id,然后 os.waitid() os.waitpid(gid, 0) ,也没用。

    更新 :

    对于一些编辑来说 xed vim gvim 两者都失败了。

    1 回复  |  直到 7 年前
        1
  •  2
  •   eyllanesc    7 年前

    不知道在这种情况下使用的终端是xterm。

    from PyQt5 import QtCore, QtGui, QtWidgets
    
    
    class EditorWorker(QtCore.QObject):
        finished = QtCore.pyqtSignal()
    
        def __init__(self, command, parent=None):
            super(EditorWorker, self).__init__(parent)
            self._temp_file = QtCore.QTemporaryFile(self)
            self._process = QtCore.QProcess(self)
            self._process.finished.connect(self.on_finished)
            self._text = ""
            if self._temp_file.open():
                program, *arguments = command
                self._process.start(
                    program, arguments + [self._temp_file.fileName()]
                )
    
        @QtCore.pyqtSlot()
        def on_finished(self):
            if self._temp_file.isOpen():
                self._text = self._temp_file.readAll().data().decode()
                self.finished.emit()
    
        @property
        def text(self):
            return self._text
    
        def __del__(self):
            self._process.kill()
    
    
    class Widget(QtWidgets.QWidget):
        def __init__(self, parent=None):
            super(Widget, self).__init__(parent)
            self._button = QtWidgets.QPushButton(
                "Launch VIM", clicked=self.on_clicked
            )
            self._text_edit = QtWidgets.QTextEdit(readOnly=True)
    
            lay = QtWidgets.QVBoxLayout(self)
            lay.addWidget(self._button)
            lay.addWidget(self._text_edit)
    
        @QtCore.pyqtSlot()
        def on_clicked(self):
            worker = EditorWorker("xterm -e vim".split(), self)
            worker.finished.connect(self.on_finished)
    
        @QtCore.pyqtSlot()
        def on_finished(self):
            worker = self.sender()
            prev_cursor = self._text_edit.textCursor()
            self._text_edit.moveCursor(QtGui.QTextCursor.End)
            self._text_edit.insertPlainText(worker.text)
            self._text_edit.setTextCursor(prev_cursor)
            worker.deleteLater()
    
    
    if __name__ == "__main__":
        import sys
    
        app = QtWidgets.QApplication(sys.argv)
        w = Widget()
        w.resize(640, 480)
        w.show()
        sys.exit(app.exec_())
    

    "xterm -e vim".split()
    

    [os.environ['TERMCMD'], "--", "vim"]
    

    可能的命令:

    - xterm -e vim
    - xfce4-terminal --disable-server -x vim
    

    更新:

    QFileSystemWatcher 这是一个多平台解决方案:

    from PyQt5 import QtCore, QtGui, QtWidgets
    
    
    class EditorWorker(QtCore.QObject):
        finished = QtCore.pyqtSignal()
    
        def __init__(self, command, parent=None):
            super(EditorWorker, self).__init__(parent)
            self._temp_file = QtCore.QTemporaryFile(self)
            self._process = QtCore.QProcess(self)
            self._text = ""
            self._watcher = QtCore.QFileSystemWatcher(self)
            self._watcher.fileChanged.connect(self.on_fileChanged)
    
            if self._temp_file.open():
                self._watcher.addPath(self._temp_file.fileName())
    
                program, *arguments = command
                self._process.start(
                    program, arguments + [self._temp_file.fileName()]
                )
    
        @QtCore.pyqtSlot()
        def on_fileChanged(self):
            if self._temp_file.isOpen():
                self._text = self._temp_file.readAll().data().decode()
                self.finished.emit()
    
        @property
        def text(self):
            return self._text
    
        def __del__(self):
            self._process.kill()
    
    
    class Widget(QtWidgets.QWidget):
        def __init__(self, parent=None):
            super(Widget, self).__init__(parent)
            self._button = QtWidgets.QPushButton(
                "Launch VIM", clicked=self.on_clicked
            )
            self._text_edit = QtWidgets.QTextEdit(readOnly=True)
    
            lay = QtWidgets.QVBoxLayout(self)
            lay.addWidget(self._button)
            lay.addWidget(self._text_edit)
    
        @QtCore.pyqtSlot()
        def on_clicked(self):
            worker = EditorWorker("gnome-terminal -- vim".split(), self)
            worker.finished.connect(self.on_finished)
    
        @QtCore.pyqtSlot()
        def on_finished(self):
            worker = self.sender()
            prev_cursor = self._text_edit.textCursor()
            self._text_edit.moveCursor(QtGui.QTextCursor.End)
            self._text_edit.insertPlainText(worker.text)
            self._text_edit.setTextCursor(prev_cursor)
            worker.deleteLater()
    
    
    if __name__ == "__main__":
        import sys
    
        app = QtWidgets.QApplication(sys.argv)
        w = Widget()
        w.resize(640, 480)
        w.show()
        sys.exit(app.exec_())
    
        2
  •  1
  •   lecodesportif    7 年前

    我转载的问题是 proc 是gnome终端进程而不是vim进程。

    带psutil(便携式)

    Finds the latest editor process in the list of all running processes.

    import psutil
    def popenAndCall(onExit, popenArgs):
    
        def runInThread(onExit, popenArgs):
            tmppath=popenArgs[-1]
            editor_cmd=popenArgs[-2]  # vim
            proc = subprocess.Popen(popenArgs)
            proc.wait()
    
            # Find the latest editor process in the list of all running processes
            editor_processes = []
    
            for p in psutil.process_iter():
                try:
                    process_name = p.name()
                    if editor_cmd in process_name:
                        editor_processes.append((process_name, p.pid))
                except:
                    pass
    
            editor_proc = psutil.Process(editor_processes[-1][1])
    
            rec=editor_proc.wait()
            print('# <runInThread>: rec=', rec)
            onExit(tmppath)
            os.remove(tmppath)
            return
    
        thread = threading.Thread(target=runInThread, args=(onExit, popenArgs))
        thread.start()
        return thread
    

    不带psutil(适用于Linux,但不可移植到Mac OS或Windows)

    从中提取 https://stackoverflow.com/a/2704947/241866 source code of psutil .

    def popenAndCall(onExit, popenArgs):
    
        def runInThread(onExit, popenArgs):
            tmppath=popenArgs[-1]
            editor_cmd=popenArgs[-2]  # vim
            proc = subprocess.Popen(popenArgs)
            proc.wait()
    
            # Find the latest editor process in the list of all running processes
    
            pids = [pid for pid in os.listdir('/proc') if pid.isdigit()]
    
            editor_processes = []
            for pid in pids:
                try:
                    process_name = open(os.path.join('/proc', pid, 'cmdline'), 'rb').read().split('\0')[0]
                    if editor_cmd in process_name:
                        editor_processes.append((process_name, int(pid)))
                except IOError:
                    continue
            editor_proc_pid = editor_processes[-1][1]
    
            def pid_exists(pid):
                try:
                    os.kill(pid, 0)
                    return True
                except:
                    return 
    
            while True:
                if pid_exists(editor_proc_pid):
                    import time
                    time.sleep(1)
                else:
                    break
    
            onExit(tmppath)
            os.remove(tmppath)
            return
    
        thread = threading.Thread(target=runInThread, args=(onExit, popenArgs))
        thread.start()
        return thread
    

    def popenAndCall(onExit, popenArgs):
    
        def runInThread(onExit, popenArgs):
            tmppath=popenArgs[-1]
            proc = subprocess.Popen(popenArgs)
            # this immediately finishes OPENING vim.
            rec=proc.wait()
            raw_input("Press Enter")  # replace this with UI event
            print('# <runInThread>: rec=', rec)
            onExit(tmppath)
            os.remove(tmppath)
            return
    
        thread = threading.Thread(target=runInThread, args=(onExit, popenArgs))
        thread.start()
        return thread
    
        3
  •  0
  •   Jason    7 年前

    我认为@eyllanesc的解决方案非常接近zim正在做的事情(zim正在使用 GObject.spawn_async() GObject.child_watch_add() ,我没有经验 GObject QProcess.start() ). 但是我们遇到了一些问题,比如 gnome-terminal

    pyinotify . 我试过了 终端 , xterm , urxvt 平淡无奇 gvim

    代码如下:

    import threading
    from PyQt5 import QtCore, QtGui, QtWidgets
    import pyinotify
    
    
    class EditorWorker(QtCore.QObject):
        file_close_sig = QtCore.pyqtSignal()
        edit_done_sig = QtCore.pyqtSignal()
    
        def __init__(self, command, parent=None):
            super(EditorWorker, self).__init__(parent)
            self._temp_file = QtCore.QTemporaryFile(self)
            self._process = QtCore.QProcess(self)
            #self._process.finished.connect(self.on_file_close)
            self.file_close_sig.connect(self.on_file_close)
            self._text = ""
            if self._temp_file.open():
                program, *arguments = command
                self._process.start(
                    program, arguments + [self._temp_file.fileName()]
                )
                tmpfile=self._temp_file.fileName()
                # start a thread to monitor file saving/closing
                self.monitor_thread = threading.Thread(target=self.monitorFile,
                        args=(tmpfile, self.file_close_sig))
                self.monitor_thread.start()
    
        @QtCore.pyqtSlot()
        def on_file_close(self):
            if self._temp_file.isOpen():
                print('open')
                self._text = self._temp_file.readAll().data().decode()
                self.edit_done_sig.emit()
            else:
                print('not open')
    
        @property
        def text(self):
            return self._text
    
        def __del__(self):
            try:
                self._process.kill()
            except:
                pass
    
        def monitorFile(self, path, sig):
    
            class PClose(pyinotify.ProcessEvent):
                def my_init(self):
                    self.sig=sig
                    self.done=False
    
                def process_IN_CLOSE(self, event):
                    f = event.name and os.path.join(event.path, event.name) or event.path
                    self.sig.emit()
                    self.done=True
    
            wm = pyinotify.WatchManager()
            eventHandler=PClose()
            notifier = pyinotify.Notifier(wm, eventHandler)
            wm.add_watch(path, pyinotify.IN_CLOSE_WRITE)
    
            try:
                while not eventHandler.done:
                    notifier.process_events()
                    if notifier.check_events():
                        notifier.read_events()
            except KeyboardInterrupt:
                notifier.stop()
                return
    
    
    class Widget(QtWidgets.QWidget):
        def __init__(self, parent=None):
            super(Widget, self).__init__(parent)
            self._button = QtWidgets.QPushButton(
                "Launch VIM", clicked=self.on_clicked
            )
            self._text_edit = QtWidgets.QTextEdit(readOnly=True)
    
            lay = QtWidgets.QVBoxLayout(self)
            lay.addWidget(self._button)
            lay.addWidget(self._text_edit)
    
        @QtCore.pyqtSlot()
        def on_clicked(self):
            worker = EditorWorker(["gnome-terminal", '--', "vim"], self)
            worker.edit_done_sig.connect(self.on_edit_done)
    
        @QtCore.pyqtSlot()
        def on_edit_done(self):
            worker = self.sender()
            prev_cursor = self._text_edit.textCursor()
            self._text_edit.moveCursor(QtGui.QTextCursor.End)
            self._text_edit.insertPlainText(worker.text)
            self._text_edit.setTextCursor(prev_cursor)
            worker.deleteLater()
    
    
    if __name__ == "__main__":
        import sys
    
        app = QtWidgets.QApplication(sys.argv)
        w = Widget()
        w.resize(640, 480)
        w.show()
        sys.exit(app.exec_())
    

    pyinotify 仅在Linux中有效。 如果你能找到一个跨平台的解决方案(至少在Mac上),请告诉我。

    更新: