Как обрабатывать интерактивный CLI программно в Qt 5 для Windows - PullRequest
0 голосов
/ 03 мая 2018

У меня есть следующий интерактивный CLI -

c:\TEST> python test.py    
Running test tool.    
$help    
   |'exec <testname>' or 'exec !<testnum>'    
   |0 BQ1    
   |1 BS1    
   |2 BA1    
   |3 BP1    
$exec !2    
   |||TEST BA1_ACTIVE    
$quit    
c:\TEST>

Кто-нибудь знает, как это сделать в Qt5. Я пытаюсь QProcess, но он не обрабатывает интерактивную командную строку, показанную выше, так как exec !2 определяется пользователем.

Например, QProcess может обрабатывать python test.py, как показано ниже, однако, как мы обрабатываем команду внутри CLI, такую ​​как exec !2

QProcess *usbProcess;
usbProcess = new QProcess();

QString s = "python test.py"; 
// ??? how do we handle interactive commands, 
// such as 'exec !2' or 'exec !1' and etc ???

usbProcess->start(s);
//usbProcess->waitForReadyRead();
//usbProcess->waitForFinished();
QString text =  usbProcess->readAll();
qDebug() << text;

Ниже приведен только пример кода, и test.py должен быть таким, какой он есть! Я просто пытаюсь найти решение за пределами test.py.

"""---beginning test.py---"""

from cmd import Cmd

class MyPrompt(Cmd):

def do_help(self, args):
    if len(args) == 0:
        name = "   |'exec <testname>' or 'exec !<testnum>'\n   |0 BQ1\n   |1 BS1\n   |2 BA1\n   |3 BP1'"
    else:
        name = args
    print ("%s" % name)

def do_exec(self, args):
    if (args == "!0"):
        print ("|||TEST BQ1_ACTIVE")
    elif (args == "!1"):
        print ("|||TEST BS1_ACTIVE")
    elif (args == "!2"):
        print ("|||TEST BA1_ACTIVE")
    elif (args == "!3"):
        print ("|||TEST BP3_ACTIVE")
    else:
        print ("invalid input")

def do_quit(self, args):
    print ("Quitting.")
    raise SystemExit

if __name__ == '__main__':
    prompt = MyPrompt()
    prompt.prompt = '$ '
    prompt.cmdloop('Running test tool.')
"""---end of test.py---"""

Ответы [ 2 ]

0 голосов
/ 03 мая 2018
  1. Вся обработка должна быть асинхронной; нет waitFor звонков.

  2. Данные, поступающие с QProcess, могут быть произвольными порциями. Вам необходимо собрать все эти чанки и проанализировать их, чтобы определить, когда появится новое приглашение ввода.

  3. Процесс должен быть открыт в текстовом режиме, чтобы переводы строк переводились на \n независимо от платформы.

  4. Стандартная обработка ошибок может быть обработана с помощью QProcess.

  5. Скрипт Python не должен использовать необработанный ввод - он будет зависать в Windows. Вместо этого он должен использовать stdin / stdout и должен возвращать True в обработчике on_exit вместо того, чтобы выдавать исключение.

Во-первых, давайте выделим запрос процесса на Commander:

// https://github.com/KubaO/stackoverflown/tree/master/questions/process-interactive-50159172
#include <QtWidgets>
#include <algorithm>
#include <initializer_list>

class Commander : public QObject {
   Q_OBJECT
   QProcess m_process{this};
   QByteArrayList m_commands;
   QByteArrayList::const_iterator m_cmd = m_commands.cbegin();
   QByteArray m_log;
   QByteArray m_prompt;
   void onStdOut() {
      auto const chunk = m_process.readAllStandardOutput();
      m_log.append(chunk);
      emit hasStdOut(chunk);
      if (m_log.endsWith(m_prompt) && m_cmd != m_commands.end()) {
         m_process.write(*m_cmd);
         m_log.append(*m_cmd);
         emit hasStdIn(*m_cmd);
         if (m_cmd++ == m_commands.end())
            emit commandsDone();
      }
   }
public:
   Commander(QString program, QStringList arguments, QObject * parent = {}) :
      QObject(parent) {
      connect(&m_process, &QProcess::stateChanged, this, &Commander::stateChanged);
      connect(&m_process, &QProcess::readyReadStandardError, this, [this]{
         auto const chunk = m_process.readAllStandardError();
         m_log.append(chunk);
         emit hasStdErr(chunk);
      });
      connect(&m_process, &QProcess::readyReadStandardOutput, this, &Commander::onStdOut);
      connect(&m_process, &QProcess::errorOccurred, this, &Commander::hasError);
      m_process.setProgram(std::move(program));
      m_process.setArguments(std::move(arguments));
   }
   void setPrompt(QByteArray prompt) { m_prompt = std::move(prompt); }
   void setCommands(std::initializer_list<const char*> commands) {
      QByteArrayList l;
      l.reserve(int(commands.size()));
      for (auto c : commands) l << c;
      setCommands(l);
   }
   void setCommands(QByteArrayList commands) {
      Q_ASSERT(isIdle());
      m_commands = std::move(commands);
      m_cmd = m_commands.begin();
      for (auto &cmd : m_commands)
         cmd.append('\n');
   }
   void start() {
      Q_ASSERT(isIdle());
      m_cmd = m_commands.begin();
      m_process.start(QIODevice::ReadWrite | QIODevice::Text);
   }
   QByteArray log() const { return m_log; }
   QProcess::ProcessError error() const { return m_process.error(); }
   QProcess::ProcessState state() const { return m_process.state(); }
   int exitCode() const { return m_process.exitCode(); }
   Q_SIGNAL void stateChanged(QProcess::ProcessState);
   bool isIdle() const { return state() == QProcess::NotRunning; }
   Q_SIGNAL void hasError(QProcess::ProcessError);
   Q_SIGNAL void hasStdIn(const QByteArray &);
   Q_SIGNAL void hasStdOut(const QByteArray &);
   Q_SIGNAL void hasStdErr(const QByteArray &);
   Q_SIGNAL void commandsDone();
   ~Commander() {
      m_process.close(); // kill the process
   }
};

Тогда мы могли бы использовать регистратор, который действует как фронт для объединенного вывода журнала:

template <typename T> void forEachLine(const QByteArray &chunk, T &&fun) {
   auto start = chunk.begin();
   while (start != chunk.end()) {
      auto end = std::find(start, chunk.end(), '\n');
      auto lineEnds = end != chunk.end();
      fun(lineEnds, QByteArray::fromRawData(&*start, end-start));
      start = end;
      if (lineEnds) start++;
   }
}

class Logger : public QObject {
   Q_OBJECT
   QtMessageHandler previous = {};
   QTextCharFormat logFormat;
   bool lineStart = true;
   static QPointer<Logger> &instance() { static QPointer<Logger> ptr; return ptr; }
public:
   explicit Logger(QObject *parent = {}) : QObject(parent) {
      Q_ASSERT(!instance());
      instance() = this;
      previous = qInstallMessageHandler(Logger::logMsg);
   }
   void operator()(const QByteArray &chunk, const QTextCharFormat &modifier = {}) {
      forEachLine(chunk, [this, &modifier](bool ends, const QByteArray &chunk){
         auto text = QString::fromLocal8Bit(chunk);
         addText(text, modifier, lineStart);
         lineStart = ends;
      });
   }
   static void logMsg(QtMsgType, const QMessageLogContext &, const QString &msg) {
      (*instance())(msg.toLocal8Bit().append('\n'), instance()->logFormat);
   }
   Q_SIGNAL void addText(const QString &text, const QTextCharFormat &modifier, bool newBlock);
   void setLogFormat(const QTextCharFormat &format) { logFormat = format; }
   ~Logger() override { if (previous) qInstallMessageHandler(previous); }
};

Затем мы можем определить некоторые вспомогательные операторы для создания модифицированных QTextCharFormat:

static struct SystemFixedPitchFont_t {} constexpr SystemFixedPitchFont;
QTextCharFormat operator<<(QTextCharFormat format, const QBrush &brush) {
   return format.setForeground(brush), format;
}
QTextCharFormat operator<<(QTextCharFormat format, SystemFixedPitchFont_t) {
   return format.setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)), format;
}

Нам также нужна функция, которая добавит текст в наше представление журнала:

void addText(QPlainTextEdit *view, const QString &text, const QTextCharFormat &modifier, bool newBlock) {
   view->mergeCurrentCharFormat(modifier);
   if (newBlock)
      view->appendPlainText(text);
   else
      view->textCursor().insertText(text);
}

Наконец, демо-жгут:

int main(int argc, char *argv[]) {
   QApplication app{argc, argv};

   Commander cmdr{"python", {"test.py"}};
   cmdr.setPrompt("$ ");
   cmdr.setCommands({"help", "exec !2", "exec !0", "help", "exec !1", "exec !3", "quit"});

   QWidget w;
   QVBoxLayout layout{&w};
   QPlainTextEdit logView;
   QPushButton start{"Start"};
   Logger log{logView.document()};
   layout.addWidget(&logView);
   layout.addWidget(&start);
   logView.setMaximumBlockCount(1000);
   logView.setReadOnly(true);
   logView.setCurrentCharFormat(QTextCharFormat() << SystemFixedPitchFont);
   log.setLogFormat(QTextCharFormat() << Qt::darkGreen);

   QObject::connect(&log, &Logger::addText, &logView, [&logView](auto &text, auto &mod, auto block){
      addText(&logView, text, mod, block);
   });
   QObject::connect(&cmdr, &Commander::hasStdOut, &log, [&log](auto &chunk){ log(chunk, QTextCharFormat() << Qt::black); });
   QObject::connect(&cmdr, &Commander::hasStdErr, &log, [&log](auto &chunk){ log(chunk, QTextCharFormat() << Qt::red); });
   QObject::connect(&cmdr, &Commander::hasStdIn, &log, [&log](auto &chunk){ log(chunk, QTextCharFormat() << Qt::blue); });
   QObject::connect(&cmdr, &Commander::stateChanged, &start, [&start](auto state){
      qDebug() << state;
      start.setEnabled(state == QProcess::NotRunning);
   });
   QObject::connect(&start, &QPushButton::clicked, &cmdr, &Commander::start);

   w.show();
   return app.exec();
}

#include "main.moc"

Вывод, тогда:

screenshot

Скрипт Python:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# test.py

from __future__ import print_function
from cmd import Cmd
import time, sys

class MyPrompt(Cmd):
    def do_help(self, args):
        if len(args) == 0:
            name = "   |'exec <testname>' or 'exec !<testnum>'\n   |0 BQ1\n   |1 BS1\n   |2 BA1\n   |3 BP1"
        else:
            name = args
        print ("%s" % name)

    def do_exec(self, args):
        if (args == "!0"):
            print ("   |||TEST BQ1_ACTIVE")
        elif (args == "!1"):
            print ("   |||TEST BS1_ACTIVE")
        elif (args == "!2"):
            print ("   |||TEST BA1_ACTIVE")
        elif (args == "!3"):
            print ("   |||TEST BP3_ACTIVE")
        else:
            print ("invalid input")
        time.sleep(1)

    def do_quit(self, args):
        print ("Quitting.", file=sys.stderr)
        return True

if __name__ == '__main__':
    prompt = MyPrompt()
    prompt.use_rawinput = False
    prompt.prompt = '$ '
    prompt.cmdloop('Running test tool.')
0 голосов
/ 03 мая 2018

Сначала избегайте использования методов waitForXXX, используйте главное достоинство Qt: сигналы и слоты.

В случае QProcess вы должны использовать readyReadStandardError и readyReadStandardOutput, с другой стороны, программа не может быть "python test.py", программа - "python", а ее аргумент - "test.py".

Следующий пример был протестирован в Linux, но я думаю, что изменения, которые вы должны внести, - это указать пути к исполняемому файлу python и файлу .py

#include <QCoreApplication>
#include <QProcess>
#include <QDebug>

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    QProcess process;
    process.setProgram("/usr/bin/python");
    process.setArguments({"/home/eyllanesc/test.py"});

    // commands to execute consecutively.
    QList<QByteArray> commands = {"help", "exec !2", "exec !0", "help", "exec !1", "exec !3", "quit"};
    QListIterator<QByteArray> itr (commands);

    QObject::connect(&process, &QProcess::readyReadStandardError, [&process](){
        qDebug()<< process.readAllStandardError();
    });
    QObject::connect(&process, &QProcess::readyReadStandardOutput, [&process, &itr](){
        QString result = process.readAll();
        qDebug().noquote()<< "Result:\n" << result;
        if(itr.hasNext()){
            const QByteArray & command = itr.next();
            process.write(command+"\n");
            qDebug()<< "command: " << command;
        }
        else{
            // wait for the application to close.
            process.waitForFinished(-1);
            QCoreApplication::quit();
        }
    });

    process.start();

    return a.exec();
}

Выход:

Result:
 Running test tool.
$ 
command:  "help"
Result:
    |'exec <testname>' or 'exec !<testnum>'
   |0 BQ1
   |1 BS1
   |2 BA1
   |3 BP1'
$ 
command:  "exec !2"
Result:
 |||TEST BA1_ACTIVE
$ 
command:  "exec !0"
Result:
 |||TEST BQ1_ACTIVE
$ 
command:  "help"
Result:
    |'exec <testname>' or 'exec !<testnum>'
   |0 BQ1
   |1 BS1
   |2 BA1
   |3 BP1'
$ 
command:  "exec !1"
Result:
 |||TEST BS1_ACTIVE
$ 
command:  "exec !3"
Result:
 |||TEST BP3_ACTIVE
$ 
command:  "quit"
Result:
 Quitting.
...