【问题标题】:Combine interactive input from stdin with async output to stdout将来自标准输入的交互式输入与异步输出结合到标准输出
【发布时间】:2015-05-24 03:51:43
【问题描述】:

我的测试应用程序将日志写入stderr 并使用stdin 接收来自用户的交互式命令。不用说,任何stderr 输出都会破坏终端中的用户输入(和命令提示符)。比如这个命令行(_是光标位置):

Command: reboo_

会变成:

Command: reboo04-23 20:26:12.799 52422  2563 D run@main.cpp:27 started
_

log() 通话之后。

为了解决这个问题,我想在终端中使用类似旧的 Quake 控制台的东西,其中日志在当前输入行的上方一行。换句话说,我想得到它:

04-23 20:26:12.799 52422  2563 D run@main.cpp:27 started
Command: reboo_

我可以修改日志记录代码和读取用户输入的代码。希望它适用于 Linux 和 OS X。log() 函数可以从不同的线程调用。 log() 函数是 stderr 的唯一写入者。

欢迎提出其他解决该问题的建议(损坏的输入行)。我正在寻找一种无需额外库(如 Curses)即可实现的解决方案。我试图用谷歌搜索,但意识到我需要一种惯用的开场白来了解我到底想要什么。

更新

感谢 Jonathan Leffler 评论,我意识到我还应该提到 分离 stderrstdout 并不重要。由于我控制了log() 函数,让它写入stdout 而不是stderr 不是问题。不过,不确定它是否使任务更容易。

更新

制作了一些看起来足够好用的东西:

void set_echoctl(const int fd, const int enable)
{
    struct termios tc; 
    tcgetattr(fd, &tc);
    tc.c_lflag &= ~ECHOCTL;
    if (enable)
    {   
        tc.c_lflag |= ECHOCTL;
    }   
    tcsetattr(fd, TCSANOW, &tc);
}

void log(const char *const msg)
{
        // Go to line start
        write(1, "\r", 1);
        // Erases from the current cursor position to the end of the current line
        write(1, "\033[K", strlen("\033[K"));

        fprintf(stderr, "%s\n", msg);

        // Move cursor one line up
        write(1, "\033[1A", strlen("\033[1A"));
        // Disable echo control characters
        set_echoctl(1, 0);
        // Ask to reprint input buffer
        termios tc;
        tcgetattr(1, &tc);
        ioctl(1, TIOCSTI, &tc.c_cc[VREPRINT]);
        // Enable echo control characters back
        set_echoctl(1, 1);
}

但是,它不支持命令提示符(输入行开头的“Command:”)。但也许我可以为此设置两行 - 一行用于命令提示符,另一行用于输入本身,例如:

Command: 
reboo_

【问题讨论】:

  • 您可以重新实现 curses 会做的事情,但其他任何事情都可能导致问题。如果你排除了最接近理智的答案,你就会遇到问题。您必须非常小心地管理写给stderr 的内容,并非常小心地与写给stdout 的内容相协调。事实上,您可能需要一个全屏管理包,可能带有用于屏幕不同部分的子窗口——就像curses 为您提供的那样。如果做不到这一点,您将不得不以某种方式(单独的线程?)拦截到stderr 的所有输出并让它处理它。你能把错误写到日志文件吗?
  • 我可以在任何地方写错误,但我也需要它们在控制台上:)
  • 在这种情况下,我强烈建议您使用curses,除非您希望出于自己的目的重新实现它。否则,您必须编写代码来确定(或知道)光标的位置,将写入位置(光标)移动到您希望错误发生的行,写入错误,然后将光标移回原来的位置在你开始写错误之前。可以办到; curses 提供了自动执行此操作的机制,因此您也可以执行 curses 所做的事情。但为自己做这件事并非易事。
  • 我认为你真的需要一个 lib 或(不太便携)一个特定于操作系统的 API 来处理这类东西。否则输出将是一个连续的字符流。

标签: c++ c linux unix console


【解决方案1】:

以下是我想出的最终解决方案。它实际上是一个工作示例,它产生 N 个线程并从每个线程发出日志。同时允许交互式用户输入命令。不过,唯一支持的命令是“exit”。其他命令将被静默忽略。它有两个小缺陷(就我而言)。

第一个是命令提示符必须在单独的行上。像这样:

Command:
reboo_

原因是VREPRINT 控制字符也会发出一个新行。所以我没有找到如何在没有新行的情况下重新打印当前输入缓冲区的方法。

是在打印日志行的同时输入符号时偶尔会闪烁。但尽管闪烁,最终结果是一致的,并且没有观察到线条重叠。也许我以后会想办法避免它,让它变得光滑干净,但已经足够了。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/termios.h>
#include <sys/ioctl.h>

static const char *const c_prompt = "Command: ";
static pthread_mutex_t g_stgout_lock = PTHREAD_MUTEX_INITIALIZER;

void log(const char *const msg)
{
    pthread_mutex_lock(&g_stgout_lock);
    // \033[1A - move cursor one line up
    // \r      - move cursor to the start of the line
    // \033[K  - erase from cursor to the end of the line
    const char preface[] = "\033[1A\r\033[K";
    write(STDOUT_FILENO, preface, sizeof(preface) - 1);

    fprintf(stderr, "%s\n", msg);
    fflush(stdout);

    const char epilogue[] = "\033[K";
    write(STDOUT_FILENO, epilogue, sizeof(epilogue) - 1);

    fprintf(stdout, "%s", c_prompt);
    fflush(stdout);

    struct termios tc;
    tcgetattr(STDOUT_FILENO, &tc);
    const tcflag_t lflag = tc.c_lflag;
    // disable echo of control characters
    tc.c_lflag &= ~ECHOCTL;
    tcsetattr(STDOUT_FILENO, TCSANOW, &tc);
    // reprint input buffer
    ioctl(STDOUT_FILENO, TIOCSTI, &tc.c_cc[VREPRINT]);
    tc.c_lflag = lflag;
    tcsetattr(STDOUT_FILENO, TCSANOW, &tc);

    pthread_mutex_unlock(&g_stgout_lock);
}

void *thread_proc(void *const arg)
{
    const size_t i = (size_t)arg;
    char ts[16];
    char msg[64];
    for (;;)
    {
        const useconds_t delay = (1.0 + rand() / (double)RAND_MAX) * 1000000;
        pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, 0);
        usleep(delay);
        pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, 0);
        time_t t;
        time(&t);
        ts[strftime(ts, sizeof(ts), "%T", localtime(&t))] = 0;
        snprintf(msg, sizeof(msg), "%s - message from #%zu after %lluns",
                 ts, i, (unsigned long long)delay);
        log(msg);
    }
}


int main()
{
    const size_t N = 4;
    pthread_t threads[N];
    for (size_t i = N; 0 < i--;)
    {
        pthread_create(threads + i, 0, thread_proc, (void *)i);
    }
    char *line;
    size_t line_len;
    for (;;)
    {
        pthread_mutex_lock(&g_stgout_lock);
        fprintf(stdout, "%s\n", c_prompt);
        fflush(stdout);
        pthread_mutex_unlock(&g_stgout_lock);
        line = fgetln(stdin, &line_len);
        if (0 == line)
        {
            break;
        }
        if (0 == line_len)
        {
            continue;
        }
        line[line_len - 1] = 0;
        line[strcspn(line, "\n\r")] = 0;
        if (0 == strcmp("exit", line))
        {
            break;
        }
    }
    for (size_t i = N; 0 < i--;)
    {
        pthread_cancel(threads[i]);
        pthread_join(threads[i], 0);
    }
    return 0;
}

所用相关文档的链接:

【讨论】:

    【解决方案2】:

    这是我做的事情。打开 3 个控制台:

    控制台#1:(运行程序,输入std::cin)

    > ./program > output.txt 2> errors.txt
    

    控制台 #2:(查看 std::cout)

    > tail -f output.txt
    

    控制台 #3:(查看 std::cerr)

    > tail -f errors.txt
    

    任何程序输入都输入到控制台:#1

    您可以获得一些控制台,例如 Terminator,它允许您将屏幕分成单独的部分:

    【讨论】:

    • 我想要一个不需要用户执行任何操作或特定环境设置的解决方案。我猜你的建议在其他情况下可能有用,但不能回答最初的问题。
    【解决方案3】:

    从更新到您可能希望使用readline 库查看的问题:

    它将用户输入的底线划分出来,并将所有内容输出到它上面的行。它还提供了可配置的提示,甚至还具有记录输入历史的功能。

    下面是一个示例,您可以从中汲取灵感,为您的 log() 函数提供灵感:

    #include <cstdlib>
    #include <memory>
    #include <iostream>
    #include <algorithm>
    
    #include <readline/readline.h>
    #include <readline/history.h>
    
    struct malloc_deleter
    {
        template <class T>
        void operator()(T* p) { std::free(p); }
    };
    
    using cstring_uptr = std::unique_ptr<char, malloc_deleter>;
    
    std::string& trim(std::string& s, const char* t = " \t")
    {
        s.erase(s.find_last_not_of(t) + 1);
        s.erase(0, s.find_first_not_of(t));
        return s;
    }
    
    int main()
    {
        using_history();
        read_history(".history");
    
        std::string shell_prompt = "> ";
    
        cstring_uptr input;
        std::string line, prev;
    
        input.reset(readline(shell_prompt.c_str()));
    
        while(input && trim(line = input.get()) != "exit")
        {
            if(!line.empty())
            {
                if(line != prev)
                {
                    add_history(line.c_str());
                    write_history(".history");
                    prev = line;
                }
    
                std::reverse(line.begin(), line.end());
                std::cout << line << '\n';
            }
            input.reset(readline(shell_prompt.c_str()));
        }
    
    }
    

    这个简单的例子只是反转你在控制台输入的所有内容。

    【讨论】:

    • 不幸的是,我为该项目选择的库非常有限。此外,在提供更好的用户体验的同时,readline 库本身并不能解决原始问题。
    • @wonder.mice 我认为 readline 非常普遍,bash shell 本身使用它作为输入。所以如果平台有bash,那么它就有readline
    • 例如,Android NDK 没有libreadline。不幸的是,可用性不是主要问题。库还具有其他特征,例如内存占用、代码大小、许可证、依赖项等。Readline 增加了约 200Kb 的内存占用,这对于该项目来说太多了。同样,它并没有解决原来的问题。
    猜你喜欢
    • 2013-11-12
    • 2011-12-17
    • 2013-09-18
    • 1970-01-01
    • 1970-01-01
    • 2021-10-20
    • 1970-01-01
    • 2017-02-03
    • 1970-01-01
    相关资源
    最近更新 更多