如何区分Escape和Escape Sequence

时间:2022-06-24 05:54:38

My end goal is to distinguish between my pressing Esc (ASCII 27) on my keyboard, and me pressing the key on my keyboard (which translates to a sequence of 27 91 67). I am using termios to put my terminal into non-Canonical mode.

我的最终目标是区分我在键盘上按下Esc(ASCII 27),然后按下我键盘上的→键(转换为27 91 67的序列)。我正在使用termios将我的终端设置为非Canonical模式。

I think I understand there are two options:

我想我明白有两种选择:

  • Wait some arbitrary amount of time to see if something comes in (seems hacky)
  • 等待一段任意的时间来看看是否有东西进入(似乎是hacky)

  • Check STDIN to see if it is empty
  • 检查STDIN以查看它是否为空

I'm trying to do the latter. To that end, I'm trying to use select to see if stdin is empty or not.

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

The Problem

select always seems to return 0 (timeout expires). That seems odd for two reasons:

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

  1. I figured that if I didn't type anything after hitting Esc, then it would return -1 since it doesn't see anything left in stdin to read
  2. 我想如果我在击中Esc之后没有输入任何内容,那么它将返回-1,因为它没有看到stdin中有任何东西要读

  3. I figured that if I typed , then I would get a 1 returned since it sees that right after 27 there is a 91 and a 67 to read
  4. 我想如果我输入→,那么我会得到一个1,因为它看到27之后就有91和67来读

Neither of those things happens, so I'm afraid I just don't understand select or standard in/out like I thought I did.

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

The Question(s)

Why doesn't select return anything except 0 in my example? Is it possible to check if stdin is empty? How do other libraries handle this?

为什么在我的例子中没有选择返回0以外的任何东西?是否可以检查标准输入是否为空?其他库如何处理这个?

Minimal, Complete, and Verifiable Example

I am running this on both MacOS High Sierra and Ubuntu 16 with equal results.

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

Source:

#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;
}

Output

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 个解决方案

#1


3  

I think the problem is that you're using getchar() — a function from the standard I/O library — where you need to use file descriptor I/O (read()).

我认为问题是你正在使用getchar() - 标准I / O库中的一个函数 - 你需要使用文件描述符I / O(read())。

Simple example

Here's a straight-forward adaptation of your code (tested on a MacBook Pro running macOS High Sierra 10.13.2), that produces the answer you and I want.

这是您的代码的直接改编(在运行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;
}

Sample output (program esc29):

示例输出(程序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)
$

I pressed the left/right arrow keys and got 'possible sequence' reported; I pressed the ESC on the touch strip and got 'ESC key standalone'. Other characters seem plausible, the code was rigged to break when control-D is pressed.

我按下左/右箭头键并报告了“可能的顺序”;我按下触摸条上的ESC,然后“ESC键独立”。其他角色看似合理,当按下control-D时,代码被操纵破坏。

Complex example

This code reads up to 4 characters at a time, and processes those that are received. There are two nested loops, so I use a goto end_loops; (twice!) to break out of the both loops from the inner loop. I also use the atexit() function to do most of what can be done to ensure that the terminal attributes are reset to the sane state even if the program doesn't exit via the main() program. (We can debate whether the code should also use the at_quick_exit() function — it's a feature of C11 but not of POSIX.)

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

If the code reads multiple characters, it scans through them, looking for ESC (escape). If it finds one and there is any data left, then it reports the escape sequence (presumably a function key sequence). If it doesn't find any more characters, it uses select() as before to decide whether there are more characters in an ESC sequence or if this is a standalone ESC. In practice, the computer is hugely faster than a mere human, so it either reads a single character or the complete sequence. I use an array of length 4 since I think that's longer than the longest key sequence generated from the keyboard; I'd be happy to increase it to 8 (or any other larger number). The only downside to this is that the buffer must be available where characters need to be read in the unlikely event that several characters are read (e.g. because the program was computing while input was accumulating). There's also a chance that the ESC from a function key or arrow key will be the last character that fits in the buffer — in which case, extra reading is necessary. Good luck in demonstrating that with this program as written — you're not a fast enough typist. You'd need to add sleep code somewhere to allow characters to accumulate before it reads them.

如果代码读取多个字符,它会扫描它们,寻找ESC(转义)。如果找到一个并且还有任何数据,则它会报告转义序列(可能是一个功能键序列)。如果找不到任何其他字符,它会像以前一样使用select()来判断ESC序列中是否有更多字符,或者这是否是一个独立的ESC。在实践中,计算机比单纯的人快得多,因此它可以读取单个字符或完整序列。我使用长度为4的数组,因为我认为它长于键盘生成的最长键序列;我很乐意把它增加到8(或任何其他更大的数字)。唯一的缺点是,在不太可能发生读取多个字符的情况下,缓冲区必须是可用的,其中需要读取字符(例如,因为程序在输入累积时计算)。从功能键或箭头键开始的ESC也可能是适合缓冲区的最后一个字符 - 在这种情况下,需要额外的读数。祝你好好用这个程序来表明 - 你不是一个足够快的打字员。您需要在某处添加睡眠代码以允许字符在读取之前累积。

So, this mainly shows a couple of extra techniques, but it could be useful as an alternative way of thinking about the processing.

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

#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;
}

Sample output (program esc67):

示例输出(程序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
$

#1


3  

I think the problem is that you're using getchar() — a function from the standard I/O library — where you need to use file descriptor I/O (read()).

我认为问题是你正在使用getchar() - 标准I / O库中的一个函数 - 你需要使用文件描述符I / O(read())。

Simple example

Here's a straight-forward adaptation of your code (tested on a MacBook Pro running macOS High Sierra 10.13.2), that produces the answer you and I want.

这是您的代码的直接改编(在运行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;
}

Sample output (program esc29):

示例输出(程序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)
$

I pressed the left/right arrow keys and got 'possible sequence' reported; I pressed the ESC on the touch strip and got 'ESC key standalone'. Other characters seem plausible, the code was rigged to break when control-D is pressed.

我按下左/右箭头键并报告了“可能的顺序”;我按下触摸条上的ESC,然后“ESC键独立”。其他角色看似合理,当按下control-D时,代码被操纵破坏。

Complex example

This code reads up to 4 characters at a time, and processes those that are received. There are two nested loops, so I use a goto end_loops; (twice!) to break out of the both loops from the inner loop. I also use the atexit() function to do most of what can be done to ensure that the terminal attributes are reset to the sane state even if the program doesn't exit via the main() program. (We can debate whether the code should also use the at_quick_exit() function — it's a feature of C11 but not of POSIX.)

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

If the code reads multiple characters, it scans through them, looking for ESC (escape). If it finds one and there is any data left, then it reports the escape sequence (presumably a function key sequence). If it doesn't find any more characters, it uses select() as before to decide whether there are more characters in an ESC sequence or if this is a standalone ESC. In practice, the computer is hugely faster than a mere human, so it either reads a single character or the complete sequence. I use an array of length 4 since I think that's longer than the longest key sequence generated from the keyboard; I'd be happy to increase it to 8 (or any other larger number). The only downside to this is that the buffer must be available where characters need to be read in the unlikely event that several characters are read (e.g. because the program was computing while input was accumulating). There's also a chance that the ESC from a function key or arrow key will be the last character that fits in the buffer — in which case, extra reading is necessary. Good luck in demonstrating that with this program as written — you're not a fast enough typist. You'd need to add sleep code somewhere to allow characters to accumulate before it reads them.

如果代码读取多个字符,它会扫描它们,寻找ESC(转义)。如果找到一个并且还有任何数据,则它会报告转义序列(可能是一个功能键序列)。如果找不到任何其他字符,它会像以前一样使用select()来判断ESC序列中是否有更多字符,或者这是否是一个独立的ESC。在实践中,计算机比单纯的人快得多,因此它可以读取单个字符或完整序列。我使用长度为4的数组,因为我认为它长于键盘生成的最长键序列;我很乐意把它增加到8(或任何其他更大的数字)。唯一的缺点是,在不太可能发生读取多个字符的情况下,缓冲区必须是可用的,其中需要读取字符(例如,因为程序在输入累积时计算)。从功能键或箭头键开始的ESC也可能是适合缓冲区的最后一个字符 - 在这种情况下,需要额外的读数。祝你好好用这个程序来表明 - 你不是一个足够快的打字员。您需要在某处添加睡眠代码以允许字符在读取之前累积。

So, this mainly shows a couple of extra techniques, but it could be useful as an alternative way of thinking about the processing.

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

#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;
}

Sample output (program esc67):

示例输出(程序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
$