c++ - 将来自 stdin 的交互式输入与到 stdout 的异步输出相结合

标签 c++ c linux unix console

我的测试应用程序将日志写入 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 的评论,我意识到我还应该提到 separating 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: 
reboo_

最佳答案

下面是我想出的最终解决方案。它实际上是一个生成 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;
}

所用相关文档的链接:

关于c++ - 将来自 stdin 的交互式输入与到 stdout 的异步输出相结合,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/30419899/

相关文章:

C: 如何检查两个 [x][y] 坐标是否匹配?

linux - awk 或 perl one liner + 使用 awk one liner 命令验证数组值

c++ - 通过允许用户选择顶点数使用 Boost 库生成图形

c++ - 在 C++ 中使用数组仅存储类的数据成员的名称

c - "foreground process group"是控制终端的属性还是 session 的属性?

linux - 如何使用 Packer 创建带有 Linux 自定义分区的 Azure VM

linux - Shell 脚本 - 读取属性文件和两个变量的加法(数学)

c++ - "inString not declared in this scope"

c++ - 将 char 数组转换为 long 有什么作用?

c - 浮点精度的细微差别