如何区分Escape和Escape Sequence

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

我想我明白有两种选择:

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

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

问题

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

  1. 我想如果我在击中Esc之后没有输入任何内容,那么它将返回-1,因为它没有看到stdin中有任何东西要读
  2. 我想如果我输入 ,那么我会得到一个1因为它看到27之后就有9167来读

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

问题

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

最小,完整和可validation的示例

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

资源:

 #include  #include  #include  #include  #include  #include  #include  #include  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 

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

简单的例子

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

 #include  #include  #include  #include  #include  #include  #include  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键独立”。 其他角色看似合理,当按下control-D时,代码被操纵破坏。

复杂的例子

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

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

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

 #include  #include  #include  #include  #include  #include  #include  #include  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 $