如何区分Escape和Escape Sequence

时间:2017-12-31 03:33:15

标签: c select posix ansi-escape termios

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

我想我明白有两种选择:

  • 等待一段任意的时间来看看是否有东西进入(似乎是hacky)
  • 检查STDIN是否为空

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

问题

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

  1. 我想如果我在点击 Esc 之后没有输入任何内容,那么它会返回-1,因为它没有看到stdin中剩下的任何东西要读
  2. 我想如果我输入,那么我会收到1,因为它会在27之后看到9167阅读
  3. 这些都没有发生,所以我担心我不会像我想象的那样理解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 个答案:

答案 0 :(得分:3)

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

简单示例

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

我按下左/右箭头键,得到了可能的顺序&#39;报道;我按下了触摸条上的ESC并且单独使用了ESC键&#39;。其他角色看似合理,当按下control-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;
}

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