Как обрабатывать интерактивный CLI программно в Qt 5 для Windows

У меня есть следующий интерактивный 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---"""

1

Решение

Во-первых, избегайте использования методов 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.
1

Другие решения

  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"

Вывод, то:

Скриншот

Скрипт 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.')
1

По вопросам рекламы [email protected]