为什么这段代码应该从stdin读取为空?

时间:2016-07-15 15:54:30

标签: c macos io terminal

代码来自Kilo项目:

/*
 * Use write() and terminal escape (ESC) sequence
 * to query cursor position.
 */

#include <stdio.h>
#include <unistd.h>

#define ESC 27

int main(int argc, char const *argv[]) {
    char buf[32];
    unsigned int i = 0;
    int rows, cols;

    write(STDOUT_FILENO, "\033[6n", 4);

    while (i < sizeof(buf)-1) {
        if (read(STDIN_FILENO,buf+i,1) != 1) break;
        if (buf[i] == 'R') break;
        i++;
    }
    buf[i] = '\0';

    /* Parse it. */
    if (buf[0] != ESC || buf[1] != '[') return -1;
    if (sscanf(buf+2,"%d;%d",&rows,&cols) != 2) return -1;
    printf("%d %d\n", rows, cols);

    return 0;
}

OS X terminal.app中的结果是:

➜  /Users/name/Desktop/demo ./write
^[[44;1R
44 1
➜  /Users/name/Desktop/demo 
➜  /Users/name/Desktop/demo 

结果有5行,但我无法理解这些行为:

  1. 为什么在第2行输出这些字符?因为write()函数已将一些字符写入STDOUT?如果是,我该如何抑制此输出?

  2. 为什么read()函数可以从STDIN读取内容?我认为STDIN现在应该是空的,因为我没有按任何键。

  3. 在第2行之后,它会阻止。为什么我必须按 ENTER

  4. 为什么第4行是空的?因为我刚按下的 ENTER

  5. 在OS X 10.11.6 Terminal.app

    中运行

2 个答案:

答案 0 :(得分:5)

转义序列\e[6n用于向终端查询光标位置。终端通过输入字符来回答,好像它们是由用户键入的一样。答案格式为\e[y;xR,其中y为行号,x为列号,均为1。这里\e[44;1R由终端以线路模式回显,就像任何其他用户输入一样。请注意,转义字节的回显为^[。您可以使用退格键编辑此输出。尝试通过将列号更改为^[[44;11R来修改列号,并在按ENTER时查看效果。

程序尝试读取此输入但由于终端处于线路模式,您需要输入ENTER键以输入整行输入以供系统使用。

请注意,程序通过单个read()系统调用逐字节地从系统读取此输入。它在获得R时停止读取,因此换行符保留在系统缓冲区中。

该程序sscanf()解析\e[44;1,打印44 1(第44行,第1列)并退出。

shell回显命令提示符,读取待处理的换行符并回显另一个命令提示符。

我不确定如何抑制line2输出。我怀疑重定向stdout会有所作为。

stty()之前使用write()系统调用将终端设置为原始模式应该会阻止此输出以及需要输入密钥。如果您更改了终端模式,即线路规则,请记住在退出程序之前保存当前设置并恢复它们。

答案 1 :(得分:2)

如果您不需要返回,则需要将终端设置为规范模式。如果您希望它不回显响应字符串,则需要禁用回显。您同时使用tcsetattr执行这两项操作。添加到您的计划:

在顶部:

#include <errno.h>
#include <termios.h>

main的开头:

struct termios old, modified;

if (tcgetattr(STDIN_FILENO, &old) < 0) {
    if (errno == ENOTTY)
        fprintf(stderr, "input is not a terminal -- can't query position\n");
    else
        perror("tcgetattr");
    exit(1); }
modified = old;
modified.c_lflag &= ~(ICANON | ECHO);
tcsetattr(STDIN_FILENO, TCSANOW, &modified);

main结束时(返回前):

tcsetattr(STDIN_FILENO, TCSANOW, &old);

这会禁用规范模式并在程序开始时回显,然后恢复之前的设置。

另外,您确实希望将查询序列写入STDIN_FILENO而不是STDOUT_FILENO。这样即使你将输出重定向到某个地方它也能继续正常工作......