【问题标题】:How to handle interactive CLI programmatically in Qt 5 for Windows如何在 Qt 5 for Windows 中以编程方式处理交互式 CLI
【发布时间】:2018-10-14 00:16:25
【问题描述】:

我有以下交互式 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---"""

【问题讨论】:

    标签: c++ qt command-line-interface interactive qprocess


    【解决方案1】:

    首先避免使用waitForXXX方法,使用Qt的主要优点:信号和槽。

    QProcess的情况必须使用readyReadStandardErrorreadyReadStandardOutput,另一方面程序不能是"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)程序通过将源代码移动到类的函数而失败,并从 main() 调用函数。它很容易重现,有没有办法修复它? 2)如果每个命令都在等待某些东西,例如,“exec!1”向设备发送“命令”并等待设备的响应。看起来程序会“挂起”或者屏幕会被 UI 应用程序冻结,有没有办法从 Qt 解决这个问题?
    • 如果发生超时或者流程完成,我们如何捕获第二期的数据?
    • @cnm 你要嵌入的类是小部件还是QObject?
    • @cnm 1) 当然,因为在函数返回时所有对象都已消失;你必须把它变成一个类。 2) 一点也不 - 代码是从事件循环中驱动的,UI 将始终负责,但请删除 waitFor!
    • @cnm 你不能问关于虚构代码的问题。 “我尝试了一堂课……”——当然,你做到了,但你做错了什么。你希望我们做什么?神你的错误?如果您有新问题,请记住特定代码 - 请将其作为新问题发布。否则,如果您希望使用一些执行 USB 处理的自包含代码来更新您的问题 - 请这样做。否则你就是在浪费每个人的时间——你的我们的。
    【解决方案2】:
    1. 所有处理都应该是异步的;没有waitFor 来电。

    2. QProcess 传入的数据可以是任意块。您需要收集所有这些块,并对其进行解析以确定何时出现新的输入提示。

    3. 进程应以文本模式打开,以便换行符独立于平台转换为\n

    4. 标准错误转发可以由QProcess处理。

    5. Python 脚本不应使用原始输入——它将在 Windows 上挂起。相反,它应该使用标准输入/标准输出,并且应该在on_exit 处理程序中返回True,而不是抛出异常。

    首先,让我们将进程询问分解为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.')
    

    【讨论】:

    • 代码导致内存泄漏!我不确定它是在 Qt 还是 Windows 中。在点击“开始”按钮五次后,私有字节从 8,332K 增加到 9,368K,工作集从 23,460K 增加到 25,128K。
    • 如果您发现了泄漏,那么您知道如何修复它 - 随时向 github 存储库提交拉取请求。唉,看一些数字并不能确定泄漏。你引用的数字意义不大。也许某处存在错误。也许没有。唯一可以确定的方法是使用实​​际的泄漏检测工具。而你没有这样做。想想每次点击“开始”时代码做了什么:它需要使用更多的内存;日志增长!但是你不知道增长是否完全由日志增长来解释。因此,您不能声称泄漏 - 目前还没有。
    • 你所说的完全正确。我曾经使用 purify ,这是一个非常好的工具,不幸的是我不再拥有它了。当我发出 N 次点击信号时,我确实看到私有字节和工作集不断增长。我想你们可能已经对此有所了解并修复了它,这就是我发布该消息并查看是否有人遇到相同问题的原因。我仍然找不到泄漏的来源。根据 Qt 文档,代码本身对我来说看起来不错。
    • @cnm 在 linux 上通过 valgrind 的 memcheck 运行它,你就会知道它是否泄漏。否则就是浪费时间。
    • 是的! cmd 允许您以管理权限运行进程。
    猜你喜欢
    • 1970-01-01
    • 2018-06-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-04-16
    • 1970-01-01
    • 2021-08-31
    • 2012-08-04
    相关资源
    最近更新 更多