如何区分Escape和Escape Sequence

7
我的最终目标是区分我在键盘上按下Esc(ASCII码27)和我按下键盘上的键(它转换为一系列的27 91 67)。我正在使用termios将我的终端置于非规范模式。
我认为有两种选择:
- 等待一段任意时间以查看是否有输入(似乎很hacky)。 - 检查STDIN以查看它是否为空。
我正在尝试后者。为此,我正在尝试使用select来查看stdin是否为空。
问题 select似乎总是返回0(超时过期)。这有两个原因:
- 我想如果我在按下Esc之后没有输入任何内容,那么它会返回-1,因为它没有看到任何剩余的stdin可读内容。 - 我想如果我输入,那么我将得到一个1返回值,因为它看到27右边有一个91和一个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;
}

输出

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
1个回答

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

简单示例

下面是您的代码的简单修改版(在运行macOS High Sierra 10.13.2的MacBook Pro上进行了测试),它会产生您和我想要的答案。
#include <stdio.h>
#include <string.h>
#include <termios.h>
#include <sys/select.h>
#include <sys/time.h>
#include <unistd.h>
#include <errno.h>

enum { ESC_KEY = 27 };
enum { EOF_KEY = 4  };

int main(void)
{
    // 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 = old;
    new.c_lflag &= ~(ICANON | ECHO);
    tcsetattr(fd, TCSANOW, &new);

    // loop: get keypress and display (exit via 'x')
    //int key;
    printf("Enter a key to see the ASCII value; press x to exit.\n");
    while (1)
    {
        char key;
        if (read(STDIN_FILENO, &key, 1) != 1)
        {
            fprintf(stderr, "read error or EOF\n");
            break;
        }
        if (key == EOF_KEY)
        {
            fprintf(stderr, "%d (control-D or EOF)\n", key);
            break;
        }

        // 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)
                printf("Got ESC: possible sequence\n");
            else if (selret == -1)
                printf("error %d: %s\n", errno, strerror(errno));
            else
                printf("esc key standalone\n");
        }
        else 
            printf("%i\n", (int)key);
        if (key == 'x')
            break;
    }

    // set terminal back to canonical
    tcsetattr(fd, TCSANOW, &old);
    return 0;
}

样例输出(程序esc29):

$ ./esc29   # 27 isn't a 2-digit prime
Enter a key to see the ASCII value; press x to exit.
115
100
97
115
100
selret=1
Got ESC: possible sequence
91
68
selret=1
Got ESC: possible sequence
91
67
selret=0
esc key standalone
selret=0
esc key standalone
selret=0
esc key standalone
100
100
4 (control-D or EOF)
$

我按下左/右箭头键,得到“可能的序列”报告;我在触摸条上按下 ESC 键,得到“独立 ESC 键”。其他字符似乎也很合理,但代码被设置为在按下 Ctrl+D 时中断。
复杂例子
这段代码每次读取最多 4 个字符,并处理接收到的字符。有两个嵌套的循环,所以我使用了 goto end_loops;(两次!)来从内部循环中断退出两个循环。我还使用了 atexit() 函数来执行大部分工作,即使程序不是通过 main() 程序退出,也能确保终端属性被重置为正常状态。(我们可以辩论一下代码是否应该使用 at_quick_exit() 函数——它是 C11 的特性,但不是 POSIX 的特性。)
如果代码读取多个字符,它会扫描这些字符,寻找 ESC(转义)。如果找到一个并且还有任何数据,那么它就报告转义序列(可能是功能键序列)。如果没有找到更多字符,则像之前一样使用 select() 判断是否有更多字符在 ESC 序列中,或者这是一个独立的 ESC。实际上,计算机比纯人类快得多,因此它要么读取单个字符,要么读取完整序列。我使用长度为 4 的数组,因为我认为这比键盘生成的最长键序列还要长;我很乐意将其增加到 8(或任何其他更大的数字)。唯一的缺点是缓冲区必须在需要读取字符的地方可用,以防万一读取了几个字符(例如,因为程序在累积输入时正在计算)。还有一种可能性,即来自功能键或箭头键的 ESC 将是适合缓冲区的最后一个字符——在这种情况下,需要进行额外的读取。祝你好运,用这个程序演示这一点——你不是一个足够快的打字员。你需要在某个地方添加睡眠代码,以允许字符在读取它们之前累积。
因此,这主要展示了几个额外的技巧,但作为处理的替代思路,它也可能是有用的。
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <sys/time.h>
#include <termios.h>
#include <unistd.h>

enum { ESC_KEY = 27 };
enum { EOF_KEY = 4  };

/* These two need to be set in main() but accessed from reset_tty() */
static int fd = STDIN_FILENO;
static struct termios old;

// set terminal back to canonical
static void reset_tty(void)
{
    tcsetattr(fd, TCSANOW, &old);
}

int main(void)
{
    struct termios new;
    tcgetattr(fd, &old);
    new = old;
    new.c_lflag &= ~(ICANON | ECHO);
    tcsetattr(fd, TCSANOW, &new);
    atexit(reset_tty);      // Ensure the terminal is reset whenever possible

    printf("Enter a key to see the ASCII value; press x to exit.\n");
    char keys[4];
    int nbytes;
    while ((nbytes = read(fd, keys, sizeof(keys))) > 0)
    {
        for (int i = 0; i < nbytes; i++)
        {
            char key = keys[i];
            if (key == EOF_KEY)
            {
                fprintf(stderr, "%d (control-D or EOF)\n", key);
                goto end_loops;
            }
            else if (key == ESC_KEY && nbytes > i + 1)
            {
                printf("Got ESC sequence:");
                for (int j = i; j < nbytes; j++)
                    printf("%4d", keys[j]);
                putchar('\n');
                break;
            }
            else if (key == ESC_KEY)
            {
                fd_set set;
                struct timeval timeout;
                FD_ZERO(&set);
                FD_SET(fd, &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)
                    printf("Got ESC: possible sequence\n");
                else if (selret == -1)
                    printf("error %d: %s\n", errno, strerror(errno));
                else
                    printf("esc key standalone\n");
            }
            else 
                printf("%i\n", (int)key);
            if (key == 'x')
                goto end_loops;
        }
    }

end_loops:
    return 0;
}
< p>示例输出(程序 esc67 ):
$ ./esc67
Enter a key to see the ASCII value; press x to exit.
65
90
97
122
selret=0
esc key standalone
Got ESC sequence:  27  91  65
Got ESC sequence:  27  91  66
Got ESC sequence:  27  91  67
Got ESC sequence:  27  91  68
Got ESC sequence:  27  79  80
selret=0
esc key standalone
97
Got ESC sequence:  27  91  67
97
Got ESC sequence:  27  91  67
120
$

我可以问一下,为什么你把这个程序命名为esc29,并且有一个注释说“27不是2位数的质数”?这似乎是一个彩蛋,或者是我错过了的非常显然的事情。给程序命名为2位数的质数是好运吗? - Greg Schmit
我添加了一种替代的处理方式,可以一次性获取箭头键的整个键序列。我不确定它是否比第一种更好 - 两种方法都有影响。它还展示了atexit()的使用,如果程序可能从除main()循环之外的其他地方调用exit(),例如错误报告和退出函数,这可能非常重要。 - Jonathan Leffler
很好,我从未使用过atexit -- 在我现在正在工作的项目中,我为sigint/sigkill注册了信号处理程序以重置termios。 - Greg Schmit
我喜欢你一次性获取多个字节的方法。 - Greg Schmit
让我们在聊天中继续这个讨论 - Jonathan Leffler
显示剩余8条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接