如何区分转义和转义序列

Gre*_*ica 7 c select posix ansi-escape termios

我的最终目标是区分我在键盘上按下Esc(ASCII 27) 和我按下键盘上的键(转换为 的序列27 91 67)。我正在使用termios将我的终端置于非规范模式。

我想我明白有两种选择:

  • 等待一些任意的时间,看看是否有东西进来(看起来很糟糕)
  • 检查 STDIN 是否为空

我正在尝试做后者。为此,我试图用它select来查看是否stdin为空。

问题

select似乎总是返回 0(超时到期)。这似乎很奇怪,原因有两个:

  1. 我想如果我在点击后没有输入任何内容Esc,那么它会返回 -1,因为它没有看到标准输入中剩下的任何内容可供阅读
  2. 我想如果我输入,那么我会得到一个1返回,因为它会在27有 a91和 a 后67立即看到

这些事情都没有发生,所以恐怕我只是不理解select或标准输入/输出,就像我想象的那样。

问题

为什么select在我的示例中不返回除 0 之外的任何内容?是否可以检查是否stdin为空?其他图书馆如何处理这个问题?

最小、完整且可验证的示例

我在 MacOS High Sierra 和 Ubuntu 16 上运行它,结果相同。

来源:

#include <stdio.h>
#include <string.h>
#include <termios.h>
#include <sys/select.h>
#include <sys/types.h>
#include <sys/time.h>
#include <unistd.h>
#include <errno.h>

int main() {
        // put terminal into non-canonical mode
        struct termios old;
        struct termios new;
        int fd = 0;  // stdin
        tcgetattr(fd, &old);
        memcpy(&new, &old, sizeof(old));
        new.c_lflag &= ~(ICANON | ECHO);
        tcsetattr(fd, TCSANOW, &new);

        // loop: get keypress and display (exit via 'x')
        char key;
        printf("Enter a key to see the ASCII value; press x to exit.\n");
        while (1) {
                key = getchar();

                // check if ESC
                if (key == 27) {
                        fd_set set;
                        struct timeval timeout;
                        FD_ZERO(&set);
                        FD_SET(STDIN_FILENO, &set);
                        timeout.tv_sec = 0;
                        timeout.tv_usec = 0;
                        int selret = select(1, &set, NULL, NULL, &timeout);
                        printf("selret=%i\n", selret);
                        if (selret == 1) {
                                // input available
                                printf("possible sequence\n");
                        } else if (selret == -1) {
                                // error
                                printf("err=%s\n", strerror(errno));
                        } else {
                                // just esc key
                                printf("esc key standalone\n");
                        }
                }

                printf("%i\n", (int)key);
                if (key == 'x') { break; }
        }

        // set terminal back to canonical
        tcsetattr(fd, TCSANOW, &old);
        return 0;
}
Run Code Online (Sandbox Code Playgroud)

输出

gns-mac1:sandbox gns$ ./seltest 
Enter a key to see the ASCII value; press x to exit.
selret=0
esc key standalone
27
selret=0
esc key standalone
27
91
67
120
Run Code Online (Sandbox Code Playgroud)

Jon*_*ler 4

我认为问题在于您正在使用getchar()标准 I/O 库 \xe2\x80\x94 中的 \xe2\x80\x94 函数,您需要在其中使用文件描述符 I/O ( read())。

\n\n

简单的例子

\n\n

这是对您的代码的直接改编(在运行 macOS High Sierra 10.13.2 的 MacBook Pro 上测试),它产生了您和我想要的答案。

\n\n
#include <stdio.h>\n#include <string.h>\n#include <termios.h>\n#include <sys/select.h>\n#include <sys/time.h>\n#include <unistd.h>\n#include <errno.h>\n\nenum { ESC_KEY = 27 };\nenum { EOF_KEY = 4  };\n\nint main(void)\n{\n    // put terminal into non-canonical mode\n    struct termios old;\n    struct termios new;\n    int fd = 0;      // stdin\n    tcgetattr(fd, &old);\n    //memcpy(&new, &old, sizeof(old));\n    new = old;\n    new.c_lflag &= ~(ICANON | ECHO);\n    tcsetattr(fd, TCSANOW, &new);\n\n    // loop: get keypress and display (exit via \'x\')\n    //int key;\n    printf("Enter a key to see the ASCII value; press x to exit.\\n");\n    while (1)\n    {\n        char key;\n        if (read(STDIN_FILENO, &key, 1) != 1)\n        {\n            fprintf(stderr, "read error or EOF\\n");\n            break;\n        }\n        if (key == EOF_KEY)\n        {\n            fprintf(stderr, "%d (control-D or EOF)\\n", key);\n            break;\n        }\n\n        // check if ESC\n        if (key == 27)\n        {\n            fd_set set;\n            struct timeval timeout;\n            FD_ZERO(&set);\n            FD_SET(STDIN_FILENO, &set);\n            timeout.tv_sec = 0;\n            timeout.tv_usec = 0;\n            int selret = select(1, &set, NULL, NULL, &timeout);\n            printf("selret=%i\\n", selret);\n            if (selret == 1)\n                printf("Got ESC: possible sequence\\n");\n            else if (selret == -1)\n                printf("error %d: %s\\n", errno, strerror(errno));\n            else\n                printf("esc key standalone\\n");\n        }\n        else \n            printf("%i\\n", (int)key);\n        if (key == \'x\')\n            break;\n    }\n\n    // set terminal back to canonical\n    tcsetattr(fd, TCSANOW, &old);\n    return 0;\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

示例输出(程序esc29):

\n\n
$ ./esc29   # 27 isn\'t a 2-digit prime\nEnter a key to see the ASCII value; press x to exit.\n115\n100\n97\n115\n100\nselret=1\nGot ESC: possible sequence\n91\n68\nselret=1\nGot ESC: possible sequence\n91\n67\nselret=0\nesc key standalone\nselret=0\nesc key standalone\nselret=0\nesc key standalone\n100\n100\n4 (control-D or EOF)\n$\n
Run Code Online (Sandbox Code Playgroud)\n\n

我按下左/右箭头键并报告了“可能的序列”;我按下触摸条上的 ESC 并得到“ESC 键独立”。其他字符似乎是合理的,当按下 control-D 时,代码会被破坏。

\n\n

复杂的例子

\n\n

此代码一次最多读取 4 个字符,并处理收到的字符。有两个嵌套循环,因此我使用goto end_loops;(两次!)从内部循环中跳出这两个循环。我还使用该atexit()函数来完成大部分可以完成的操作,以确保即使程序没有通过程序退出,终端属性也重置为正常状态main()。(我们可以争论代码是否也应该使用at_quick_exit()函数 \xe2\x80\x94 它是 C11 的功能,但不是 POSIX 的功能。)

\n\n

如果代码读取多个字符,它会扫描它们,寻找ESC(转义)。如果它找到一个并且还剩下任何数据,那么它会报告转义序列(可能是功能键序列)。如果找不到更多字符,它将select()像以前一样使用来决定 ESC 序列中是否还有更多字符,或者这是否是一个独立的 ESC。实际上,计算机比人类快得多,因此它要么读取单个字符,要么读取完整的序列。我使用长度为 4 的数组,因为我认为它比键盘生成的最长按键序列长;我很乐意将其增加到 8(或任何其他更大的数字)。唯一的缺点是缓冲区必须可用,万一读取多个字符(例如,因为程序在输入累积时正在计算),则需要读取字符。功能键或箭头键的 ESC 也有可能是适合缓冲区 \xe2\x80\x94 的最后一个字符,在这种情况下,需要额外读取。祝你好运,通过这个编写为 \xe2\x80\x94 的程序来证明你的打字速度不够快。您需要在某处添加睡眠代码,以允许字符在读取之前累积。

\n\n

因此,这主要展示了一些额外的技术,但它可以作为思考处理的替代方法。

\n\n
#include <errno.h>\n#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n#include <sys/select.h>\n#include <sys/time.h>\n#include <termios.h>\n#include <unistd.h>\n\nenum { ESC_KEY = 27 };\nenum { EOF_KEY = 4  };\n\n/* These two need to be set in main() but accessed from reset_tty() */\nstatic int fd = STDIN_FILENO;\nstatic struct termios old;\n\n// set terminal back to canonical\nstatic void reset_tty(void)\n{\n    tcsetattr(fd, TCSANOW, &old);\n}\n\nint main(void)\n{\n    struct termios new;\n    tcgetattr(fd, &old);\n    new = old;\n    new.c_lflag &= ~(ICANON | ECHO);\n    tcsetattr(fd, TCSANOW, &new);\n    atexit(reset_tty);      // Ensure the terminal is reset whenever possible\n\n    printf("Enter a key to see the ASCII value; press x to exit.\\n");\n    char keys[4];\n    int nbytes;\n    while ((nbytes = read(fd, keys, sizeof(keys))) > 0)\n    {\n        for (int i = 0; i < nbytes; i++)\n        {\n            char key = keys[i];\n            if (key == EOF_KEY)\n            {\n                fprintf(stderr, "%d (control-D or EOF)\\n", key);\n                goto end_loops;\n            }\n            else if (key == ESC_KEY && nbytes > i + 1)\n            {\n                printf("Got ESC sequence:");\n                for (int j = i; j < nbytes; j++)\n                    printf("%4d", keys[j]);\n                putchar(\'\\n\');\n                break;\n            }\n            else if (key == ESC_KEY)\n            {\n                fd_set set;\n                struct timeval timeout;\n                FD_ZERO(&set);\n                FD_SET(fd, &set);\n                timeout.tv_sec = 0;\n                timeout.tv_usec = 0;\n                int selret = select(1, &set, NULL, NULL, &timeout);\n                printf("selret=%i\\n", selret);\n                if (selret == 1)\n                    printf("Got ESC: possible sequence\\n");\n                else if (selret == -1)\n                    printf("error %d: %s\\n", errno, strerror(errno));\n                else\n                    printf("esc key standalone\\n");\n            }\n            else \n                printf("%i\\n", (int)key);\n            if (key == \'x\')\n                goto end_loops;\n        }\n    }\n\nend_loops:\n    return 0;\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

示例输出(程序esc67):

\n\n
$ ./esc67\nEnter a key to see the ASCII value; press x to exit.\n65\n90\n97\n122\nselret=0\nesc key standalone\nGot ESC sequence:  27  91  65\nGot ESC sequence:  27  91  66\nGot ESC sequence:  27  91  67\nGot ESC sequence:  27  91  68\nGot ESC sequence:  27  79  80\nselret=0\nesc key standalone\n97\nGot ESC sequence:  27  91  67\n97\nGot ESC sequence:  27  91  67\n120\n$\n
Run Code Online (Sandbox Code Playgroud)\n