有啥方法可以在规范模式下处理转义键?

Posted

技术标签:

【中文标题】有啥方法可以在规范模式下处理转义键?【英文标题】:Any way to process escape key in canonical mode?有什么方法可以在规范模式下处理转义键? 【发布时间】:2014-12-08 15:11:34 【问题描述】:

在 unix 纯 C termios 编程中,如果我使用规范模式接收用户的一行输入,我该如何处理转义键?通常,如果用户正在输入一行文本并按下转义键,则不会发生任何事情。如果用户按转义键,我想取消当前输入。我知道我可以处理单个字符,但是我失去了规范模式的所有好处(退格等)。

【问题讨论】:

为什么不使用像readline 这样的东西,它可以让您充分控制输入处理? 将kill char设置为ESC?但是当你这样做时要小心。您应该在程序退出之前恢复用户的原始偏好。 readline 已经将 Esc 键用于一组精心设计的目的,这些目的与我想要完成的目标完全相切。我这里的 UI 是用于数据输入,而不是命令处理,所以不希望有一个复杂的行编辑器;我只需要基本的规范行为,但要处理转义键。 【参考方案1】:

原创

感谢Jonathan Leffler for his comment that hinted me at the right direction,底部是我带注释的第一个termios 程序演示者(谢谢!)。

关键是在当前终端的文件描述符上使用tcgetattr(ttyfd, &attributes) 将其当前属性检索到struct termios 中,编辑属性,然后使用tcsetattr(ttyfd, when, &attributes) 应用更改。

其中一个属性是“kill”字符 - 导致整个当前缓冲行被丢弃的字符。它是通过索引struct termiosc_cc 成员数组并将attr.c_cc[VKILL] 设置为任何人想要的(这里是Esc,等于八进制033)。

kill 字符应该在退出时恢复到之前的值。

#include <termios.h>
#include <fcntl.h>
#include <stdio.h>


int main()
    char   buf[80];
    int    numBytes;
    struct termios original, tattered;
    int    ttyfd;
    
    /* Open the controlling terminal. */
    ttyfd = open("/dev/tty", O_RDWR);
    if(ttyfd < 0)
        printf("Could not open tty!\n");
        return -1;
    
    
    /**
     * Get current terminal properties, save them (including current KILL char),
     * set the new KILL char, and make this the new setting.
     */
    
    tcgetattr(ttyfd, &original);
    tattered = original;
    tattered.c_cc[VKILL] = 033;/* New killchar, 033 == ESC. */
    tcsetattr(ttyfd, TCSANOW, &tattered);
    
    /**
     * Simple test to see whether it works.
     */
    
    write(1, "Please enter a line: ", 21);
    numBytes = read(0, buf, sizeof buf);
    write(1, buf, numBytes);
    
    /**
     * Restore original settings.
     */
    
    tcsetattr(ttyfd, TCSANOW, &original);
     
    /* Clean up. */
    close(ttyfd);
    
    return 0;

此演示似乎可以在 Mac OS X 10.6.8 上运行。我也在 Linux 上对此进行了测试,显然 Esc 杀死缓冲区似乎是默认设置,就好像我打印出c_cc[VKILL] 我得到27 == 033 == ESC

编辑

以下尝试尽可能模仿您在评论中描述的行为。它将c_cc[VEOL2] 设置为Esc; EOL2 是替代的 End-of-Line。它还 删除 Esc 作为杀死字符,因为您想接收该行。

现在发生的情况是,如果按下正常的Ret,一切正常。但是,如果按下 Esc,缓冲区中的最后一个字符将设置为 Esc,这是可以测试的条件(尽管只有在首先读取和缓冲整行之后) .

以下是根据您明确说明的演示器。它等待一行输入并将其回显

&lt;CANCELLED&gt; 如果该行以 Esc 和 终止 &lt;NORMAL &gt; 如果该行以 Ret 终止。

享受吧!

#include <termios.h>
#include <fcntl.h>
#include <stdio.h>


int main()
    char   buf[80];
    int    numBytes;
    struct termios original, tattered;
    int    ttyfd;

    /* Open the controlling terminal. */
    ttyfd = open("/dev/tty", O_RDWR);
    if(ttyfd < 0)
        printf("Could not open tty!\n");
        return -1;
    

    /**
     * Get current terminal properties, save them (including current KILL char),
     * set the new KILL char, and make this the new setting.
     */

    tcgetattr(ttyfd, &original);
    tattered = original;
    tattered.c_cc[VKILL] = 0;  /* <Nada> */
    tattered.c_cc[VEOL2] = 033;/* Esc */
    tcsetattr(ttyfd, TCSANOW, &tattered);

    /**
     * Simple test to see whether it works.
     */

    fputs("Please enter a line: ", stdout);
    fflush(stdout);
    numBytes = read(0, buf, sizeof buf);
    if(buf[numBytes-1]==033)/* Last character is Esc? */
        buf[numBytes-1] = '\n';/* Substitute with newline */
        fputs("\n<CANCELLED> ", stdout);   /* Print newline to move to next line */
    else
        fputs("<NORMAL   > ", stdout);
    
    fwrite(buf, 1, numBytes, stdout);

    /**
     * Restore original settings.
     */

    tcsetattr(ttyfd, TCSANOW, &original);

    /* Clean up. */
    close(ttyfd);

    return 0;

【讨论】:

请注意,您应该真正保留 kill 字符的原始值的副本,并在程序完成后和退出之前恢复它。你会影响所有进程,包括调用它的 shell——至少在原则上是这样。我想对于已经使用终端设置的现代 shell,如果 shell 重置其设置,它很可能是“好的”——但你至少应该用stty -a 或类似的方式验证这一点。 @JonathanLeffler 这很公平;我添加了代码来恢复原始设置。但是,突出显示被破坏了;不知道为什么,就好像整个网站的L&F突然发生了微妙的变化。 如果您指的是 SO 的 L&F,那么是的,他们今天刚刚将他们不久前对 Meta Stack Overflow 所做的更改(参见 Feedback requested: Stack Overflow design update)传播到主要的 Stack Overflow。大多数情况下它是好的,但它需要一点时间来适应。 (另见Is this flat theme new or is there an option to change it back?) 泰勒在 2014 年 10 月提出了这个问题;他很可能已经克服了这个障碍,并且没有一个简单的方法来决定它是否适合他。 这是一个有用的技术,但这里的目标是取消输入,而不是清除命令行。换句话说,需要将该行返回给程序,就像按下 Enter 一样,除了因为最后一个字符是转义字符,它会提示应用程序知道它是取消。【参考方案2】:

您需要使用tcsetattr() 函数将EOF 字符设置为ESC 而不是Enter。更多详细信息请访问http://pubs.opengroup.org/onlinepubs/7908799/xbd/termios.html#tag_008_001_009

【讨论】:

【参考方案3】:

这是我的getLine() 函数的略微修改版本,用于来自用户的稳健输入。您可以查看原始 here 的详细信息,但此版本已被修改为使用 termios 内容,可以对输入进行一定程度的控制。

因为termios 的工作级别低于标准 C 输入,所以它也会影响到这一点。

首先,getLine() 函数所需的标头和返回值:

#include <termios.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>

#define OK        0
#define NO_INPUT  1
#define TOO_LONG  2
#define TERM_PROB 3

接下来,一个帮助函数将终端恢复到其原始状态,这使您可以轻松地从 getLine() 返回一个值,知道终端将保持其原始状态。

static int revertTerm (int fd, struct termios *ptio, int old, int rc) 
    // Revert the terminal to its original state then return
    // specified value.

    ptio->c_cc[VKILL] = old;
    tcsetattr (fd, TCSANOW, ptio);
    close (fd);
    return rc;

接下来是实际的getLine() 函数本身,它修改终端属性以使 ESC 成为终止字符,然后调用fgets() 以及所有额外的提示,检测缓冲区溢出,刷新输入到行尾等等。

作为此功能的一部分,在用户位于fgets() 内期间,修改后的终端行为处于活动状态,您可以使用 ESC 清除该行。

static int getLine (char *prmpt, char *buff, size_t sz) 
    int old, fd, ch, extra;
    struct termios tio;

    // Modify teminal so ESC is KILL character.

    fd = open ("/dev/tty", O_RDWR);
    if (fd < 0)
        return TERM_PROB;

    tcgetattr (fd, &tio);
    old = tio.c_cc[VKILL];
    tio.c_cc[VKILL] = 0x1b;
    tcsetattr (fd, TCSANOW, &tio);

    // Get line with buffer overrun protection.

    if (prmpt != NULL) 
        printf ("%s", prmpt);
        fflush (stdout);
    
    if (fgets (buff, sz, stdin) == NULL)
        return revertTerm (fd, &tio, old, NO_INPUT);

    // If it was too long, there'll be no newline. In that case, we flush
    // to end of line so that excess doesn't affect the next call.

    if (buff[strlen(buff)-1] != '\n') 
        extra = 0;
        while (((ch = getchar()) != '\n') && (ch != EOF))
            extra = 1;
        return revertTerm (fd, &tio, old, (extra == 1) ? TOO_LONG : OK);
    

    // Otherwise remove newline and give string back to caller.

    buff[strlen(buff)-1] = '\0';
    return revertTerm (fd, &tio, old, OK);

最后是一个测试程序,以便您检查它的行为。基本上,它允许您输入最多 20 个字符的行,然后将它们打印出来并带有状态(太长、没有输入等)。

如果在输入过程中的任何时候按ESC,它将终止该行并重新开始。

输入exit会导致程序退出。

// Test program for getLine().

int main (void) 
    int rc, done = 0;
    char buff[21];

    while (!done) 
        rc = getLine ("Enter string (ESC to clear, exit to stop)> ",
            buff, sizeof(buff));
        if (rc == NO_INPUT) 
            // Extra NL since my system doesn't output that on EOF.
            printf ("\nNo input\n");
         else if (rc == TOO_LONG) 
            printf ("Input too long [%s]\n", buff);
         else 
            done = (strcmp (buff, "exit") == 0);
            if (!done)
                printf ("OK [%s]\n", buff);
        
    

    return 0;

【讨论】:

以上是关于有啥方法可以在规范模式下处理转义键?的主要内容,如果未能解决你的问题,请参考以下文章

使用转义键关闭反应引导模式

在 Inventor 中禁用转义键

使用 jQuery 的转义键的哪个键码

找出给定键组合的相应转义序列

面试题:SQL注入漏洞总结

有啥方法可以用 NSLog 进行彩色打印?