捕获信号时强制终端不打印 Ctrl 热键

Forcing a terminal not to print Ctrl hotkeys when signals are caught

美好的一天,

我正在为我的学校用 C 编写自己的 shell,它必须尽可能类似于 bash

我必须像 bash 一样处理 Ctrl-\ 和 Ctrl-C 等信号;出于这个原因,我被允许使用 signal 函数。它工作正常,但问题是每当捕获到 Ctrl-C 信号时(从第二个捕获开始),就会打印 ^C

在网上,我找到了一个变通方法,建议在捕获到 Ctrl-C 时打印 "\b \b\b \b\nminishell$ ",这会吞噬这两个符号。问题是,由于第一次未打印 ^C,打印吞噬了我提示的两个符号,使其成为 minishell 而不是 minishell$ ,并且光标显示不正确。

现在我想出了另一个解决方法,即声明一个静态布尔值,以便在第一次调用时不打印 baskspaces。不过,这对 Ctrl-\ 没有帮助;当我尝试写入必须替换 ^\.

的两个空格时,Ctrl-\ 继续将光标向右移动

我不喜欢这些解决方法,想知道是否有办法指示终端不输出这些东西?我可以使用 tgetenttgetflagtgetnumtgetstrtgototputstcsetattrtcgetattr,已阅读他们的手册页,但似乎没有任何帮助。

这不行吗?

void signalHandler(int signo){
 if(signo==SIGINT){
  printf("\b\b  \b\b");
  fflush(NULL);
  printf("\nHello World\n");    
 }
}

在我的 shell 中似乎工作正常。第一个 printf 和 fflush 是您必须在处理程序中实现的。之后的 printf 只是我向您展示的一种方式,您可以在 ^C 未出现后做任何您想做的事情。

为什么不显示呢?在第一个 printf 中,我使用退格键和空格擦除字符。由于 stdout 默认情况下是缓冲的,我不想使用换行符,所以我手动刷新了缓冲区。

当您在终端上键入一个键时,会发生两件事

  • 该字符在此终端上回显(显示)
  • 字符被(通过线路)发送到附加程序

这两种操作都可以通过 termios/tcsetattr() 进行控制:可以发送或回显不同的字符,可以抑制某些字符等(some/most 这些操作发生在 terminal-driver 中,但这与此处无关)

演示:使用tcsetattr()控制终端的回显:


#include <stdio.h>
#include <stdlib.h>

#define _SVID_SOURCE 1
#include <termios.h>
#include <unistd.h>
#include <signal.h>

struct termios termios_save;

void reset_the_terminal(void)
{
tcsetattr(0, 0, &termios_save );
}

sig_atomic_t the_flag = 0;
void handle_the_stuff(int num)
{
char buff[4];
buff[0] = '[';
buff[2] = '0' + num%10;
num /= 10;
buff[1] = '0' + num%10;
buff[3] = ']';
write(0, buff, sizeof buff);
the_flag = 1;
}

int main (void)
{
int rc;
int ch;
struct termios termios_new;

rc = tcgetattr(0, &termios_save );
if (rc) {perror("tcgetattr"); exit(1); }

rc = atexit(reset_the_terminal);
if (rc) {perror("atexit"); exit(1); }

termios_new = termios_save;
termios_new.c_lflag &= ~ECHOCTL;
rc = tcsetattr(0, 0, &termios_new );
if (rc) {perror("tcsetattr"); exit(1); }

signal(SIGINT, handle_the_stuff);

printf("(pseudoshell)Start typing:\n" );
while(1) {
        ch = getc(stdin);
        if (the_flag) {
                printf("Saw the signal, last character was %02x\n", (unsigned) ch);
                break;
                }
        }

exit (0);
}

设置控制台这样一个软件可以拦截所有键入的字符的方法是将终端设置为原始模式。这种方式可能出现的问题是,所有不在 ASCII 0-255 space 中的键,例如 èìà 将从控制台作为字节序列,所有功能和控制键包括光标和返回 space 将不会完成任何操作,一些代码如 CRLF 和一些 ANSI 序列从输入通道读取并在输出通道上重写时可能会完成操作。

要将终端设置为原始模式,您必须使用函数 cfmakeraw,然后使用函数 tcsetattr

下面的代码实现了一个简单但不是很好实现的终端,无论如何我认为这段代码是一个很好的起点。不管怎样,代码流程和错误控制至少要安排得更好。

当键入一个键时,代码会写入进入控制台的所有 ASCII 字符序列。所有值小于 32 或大于 126 的字符都将写为 [HEX-CODE]

即在控制台按Esc会写成[1B]Ctrl+C的代码会写成[03]F1会写成[1B]OPF11会写成[1B][23~输入将是[0D].

如果你将按Ctrl+X [18]将被写入并且程序停止,但这种行为是在如您在代码中看到的 SW 控件。

这里是代码:

#include <stdio.h>      // Standard input/output definitions
#include <string.h>     // String function definitions
#include <unistd.h>     // UNIX standard function definitions
#include <fcntl.h>      // File control definitions
#include <errno.h>      // Error number definitions
#include <termios.h>    // POSIX terminal control definitions (struct termios)

#include <sys/ioctl.h> // Used for TCGETS2, which is required for custom baud rates
#include <sys/select.h> // might be used to manage select

int setAttr(int ch, int resetToOld);

#define IN 0
#define OUT 1

typedef struct TermCap
{
    int fd;
    struct termios oldTermios;
    struct termios newTermios;
    // fd_set fds; // might be used to manage select
} TermCap;

TermCap m_termCap[2];

int main()
{
    int i,ex=0;
    char msg;
    char buff[20];

    m_termCap[IN].fd=STDIN_FILENO;
    m_termCap[OUT].fd=STDOUT_FILENO;

    // Gets STDIN config and set raw config
    setAttr(IN,0);

    // Gets STDOUT config and set raw config
    setAttr(OUT,0);

    // Console loop ... the console terminates when ^X is intercepted.
    do {
        do {
            i=read(m_termCap[IN].fd,&msg,1);
            if (i>0){
                if (msg<32 || msg>126) {
                    sprintf(buff,"[%02X]",(unsigned char)msg);
                    write(m_termCap[OUT].fd,buff,4);
                    if (msg==24)
                        ex=1;
                }else{
                    write(m_termCap[OUT].fd,&msg,i);
                }
            }
            usleep(10000); // a minimal delay of 10 millisec
        } while(i>0 && !ex);
    } while(!ex);

    // Reset console to initial state.
    setAttr(IN,1);
    setAttr(OUT,1);

    printf("\r\n\nThe end!");
    return 0;
}

int setAttr(int ch, int resetToOld)
{
    int retVal=0;
    int i;

    if (!resetToOld) {
        // Read old term config
        i=tcgetattr(m_termCap[ch].fd, &m_termCap[ch].oldTermios);
        if (i==-1) {
            return 1;
        }
    }

    m_termCap[ch].newTermios = m_termCap[ch].oldTermios;

    if (!resetToOld) {
        // Terminal in raw mode
        cfmakeraw(&m_termCap[ch].newTermios);
    }

    i=tcsetattr(m_termCap[ch].fd, TCSANOW, &m_termCap[ch].newTermios);

    if (i==-1) {
        retVal = 2;
    }

    return retVal;
}