如何区分Escape和Escape Sequence
How to distinguish between Escape and Escape Sequence
我的最终目标是区分我在键盘上按 Esc (ASCII 27
) 和按 → 我键盘上的键(转换为 27 91 67
的序列)。我正在使用 termios
将我的终端置于非规范模式。
我想我明白有两个选择:
- 等待一些任意时间,看看是否有东西进来(看起来很老套)
- 检查STDIN是否为空
我正在尝试做后者。为此,我正在尝试使用 select
来查看 stdin
是否为空。
问题
select
似乎总是 return 0(超时到期)。这看起来很奇怪,原因有二:
- 我想如果我在点击 Esc 后没有输入任何内容,那么它会 return -1 因为它没有看到任何留在标准输入中的东西阅读
- 我想如果我输入 →,那么我会得到一个
1
returned,因为它会在 27
之后看到有一个91
和一个67
要读
这些事情都没有发生,所以恐怕我只是不理解 select
或标准 in/out,就像我想的那样。
问题
为什么在我的示例中 select
return 除了 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
我认为问题在于您正在使用 getchar()
— 标准 I/O 库中的一个函数 — 您需要在其中使用文件描述符 I/O (read()
).
简单的例子
这是对您的代码的直接改编(在 MacBook Pro 运行 macOS High Sierra 10.13.2 上测试),它会产生您我想要的答案。
#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)
$
我按下了 left/right 箭头键,得到了 'possible sequence' 的报告;我按了触摸条上的ESC,得到了'ESC key standalone'。其他字符似乎是合理的,代码被操纵为在按下 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
$
我的最终目标是区分我在键盘上按 Esc (ASCII 27
) 和按 → 我键盘上的键(转换为 27 91 67
的序列)。我正在使用 termios
将我的终端置于非规范模式。
我想我明白有两个选择:
- 等待一些任意时间,看看是否有东西进来(看起来很老套)
- 检查STDIN是否为空
我正在尝试做后者。为此,我正在尝试使用 select
来查看 stdin
是否为空。
问题
select
似乎总是 return 0(超时到期)。这看起来很奇怪,原因有二:
- 我想如果我在点击 Esc 后没有输入任何内容,那么它会 return -1 因为它没有看到任何留在标准输入中的东西阅读
- 我想如果我输入 →,那么我会得到一个
1
returned,因为它会在27
之后看到有一个91
和一个67
要读
这些事情都没有发生,所以恐怕我只是不理解 select
或标准 in/out,就像我想的那样。
问题
为什么在我的示例中 select
return 除了 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
我认为问题在于您正在使用 getchar()
— 标准 I/O 库中的一个函数 — 您需要在其中使用文件描述符 I/O (read()
).
简单的例子
这是对您的代码的直接改编(在 MacBook Pro 运行 macOS High Sierra 10.13.2 上测试),它会产生您我想要的答案。
#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)
$
我按下了 left/right 箭头键,得到了 'possible sequence' 的报告;我按了触摸条上的ESC,得到了'ESC key standalone'。其他字符似乎是合理的,代码被操纵为在按下 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
$