“命令终端”的实现2-字符读取及按键控制

在上篇文章中已经完成了必要的前期准备:各种与“终端”相关的接口已经完成。本文主要内容是接收“终端”的字符并存储在缓冲区中,为下一步作好准备。同时也涉及到“终端”按键的处理,以弥补约2年前定的一篇文章的不足。

一、测试效果

先看一下测试函数,如下:

int readline_test(void)
{
    static char lastcommand[CB_SIZE] = {0};
    int len;

    while (1)
    {
        len = readline(PROMPT, lastcommand);
        if (len > 0)
        {
            if (len > CB_SIZE)
            {
                myprintf("command line too large.\n");
                break;
            }
        }
        else if (len == 0)
        {
            // do nothing 
        }
        if (len == -1)
        {
            myputs("\n");
        }
        else
        {
            myprintf("YOUR INPUT: %s\n", lastcommand);
        }
    }
    return 0;
}

效果如下:

NotAShell> hello world!
YOUR INPUT: hello world!
NotAShell> help
YOUR INPUT: help
NotAShell> clear
YOUR INPUT: clear
NotAShell>

二、实现

下面逐一介绍其实现。

2.1 函数声明及宏定义相关:

#define PROMPT          "NotAShell> "
#define CB_SIZE         256  /* console buffer size */
#define MAX_CMDBUF_SIZE CB_SIZE

/* history command */
#define HIST_MAX 10
#define HIST_SIZE MAX_CMDBUF_SIZE

int readline (const char *const prompt, char* line_buf);

这里定义了“终端”提示符,缓冲区大小,保存的历史命令最大数,等等。

2.2 ASCII码定义及光标控制相关:

#define CTL_CH(c)           ((c) - 'a' + 1)

#define CTL_BACKSPACE       ('\b')
#define DEL                 ((char)255)
#define DEL7                ((char)127)
#define BACKSPACE           ((char)8)
#define CREAD_HIST_CHAR     ('!')

#define putnstr(str,n)    do {          \
    getcmd_printf ("%.*s", (int)n, str);\
} while (0)

// 将光标移到命令行首 
#define BEGINNING_OF_LINE() {       \
    while (num) {                   \
    getcmd_putch(CTL_BACKSPACE);    \
    num--;                          \
    }                               \
}

// 删除当前光标到行尾所有字符 
#define ERASE_TO_EOL() {                     \
    if (num < eol_num) {                     \
    getcmd_printf("%*s", (int)(eol_num - num), ""); \
    do {                                     \
    getcmd_putch(CTL_BACKSPACE);             \
    } while (--eol_num > num);               \
    }                                        \
}

// 将光标移到命令行尾 
#define REFRESH_TO_EOL() {      \
    if (num < eol_num) {        \
    wlen = eol_num - num;       \
    putnstr(buf + num, wlen);   \
    num = eol_num;              \
    }                           \
}

上面最重要的一个宏定义是CTL_CH,它用于判断用户输入的按键组合,代码中处理的按键有:

0)、上下左右方向键
1)、c-a: 移到命令行首
2)、c-e: 移到命令行尾
3)、c-f: 前移一字符
4)、c-b: 后移一字符
5)、c-x/c-u: 删除整行
6)、c-d: 删除光标当前字符
7)、c-k: 删除当前光标至行尾字符
8)、c-p: 前一条历史命令
9)、c-n: 后一条历史命令
10)、HOME/END: 移至行首/行尾
11)、Delete:Windows-删除当前光标字符 Linux-删除光标前字符

很多快捷键与GNU Readline库相同。实际中,Linux有很多工具、软件都使用了Readline库,像bash shell,因此,你可以在bash shell使用c-a将光标移到行首,c-e移到行尾——这一点在一个长命令中尤其有用。更多关于Readline库的快捷键,请在搜索引擎中使用关键字“Readline shortcuts”进行搜索。
上面提到的c-a等就是组合按键,c表示Ctrl键,c-a表示同时按Ctrl键和a键,从ASCII码表(请搜索多一些ASCII码表,最好用google,或者搜索笔者写的文章《u-boot移植随笔:u-boot shell与ASCII码)中可以知道,c-a的ASCII码是1,即:CTL_CH(‘a’) = ‘a’ - ‘a’ + 1。

3.3 处理普通字符

static void cread_add_char(char ichar, int insert, unsigned long *num,
                           unsigned long *eol_num, char *buf, unsigned long len)
{
    unsigned long wlen;

     /* room ??? */
    if (insert || *num == *eol_num)
    {
        if (*eol_num > len - 1)
        {
            getcmd_cbeep();
            return;
        }
        (*eol_num)++;
    }

    if (insert)
    {
        wlen = *eol_num - *num;
        if (wlen > 1)
        {
            memmove(&buf[*num+1], &buf[*num], wlen-1);
        }

        buf[*num] = ichar;
        putnstr(buf + *num, wlen);
        (*num)++;
        while (--wlen)
        {
            getcmd_putch(CTL_BACKSPACE);
        }
    }
    else
    {
         /* echo the character */
        wlen = 1;
        buf[*num] = ichar;
        putnstr(buf + *num, wlen);
        (*num)++;
    }
}

static void cread_add_str(char *str, int strsize, int insert, unsigned long *num,
                          unsigned long *eol_num, char *buf, unsigned long len)
{
    while (strsize--)
    {
        cread_add_char(*str, insert, num, eol_num, buf, len);
        str++;
    }
}

4.4 关键处理函数

这部分代码比较长,所以先说关键代码。
处理按键,包括方向键及HOME、END等,Windows及Linux按键如下:

       Windows: 
                up:     0xe0 'H'
                down:   0xe0 'P'
                right:  0xe0 'M'
                left:   0xe0 'K'

                HOME:   0xe0 'G'
                END:    0xe0 'O'(大写字母O)
                Delete: 0xe0 'S'
        Linux:
                up:     0x1b 0x5b 0x41  ^[[A
                down:   0x1b 0x5b 0x42  ^[[B
                right:  0x1b 0x5b 0x43  ^[[C
                left:   0x1b 0x5b 0x44  ^[[D
                
                HOME:   ?
                END:    ? 
                Delete: 0x7f

处理代码如下:

static int cread_line(const char *const prompt, char *buf, unsigned int *len)
{
    unsigned long num = 0;
    unsigned long eol_num = 0;
    unsigned long wlen;
    unsigned char ichar;
    int insert = 1;
    int esc_len = 0;
    char esc_save[8];
    int init_len = (int)strlen(buf);
    int cur_num = 0;

    if (init_len)
        cread_add_str(buf, init_len, 1, &num, &eol_num, buf, *len);

    while (1)
    {
        ichar = getcmd_getch();

        if ((ichar == '\n') || (ichar == '\r'))
        {
            getcmd_putch('\n');
            break;
        }

          /*
         * handle Windows arrow key, etc.
         * note: arrow keys have two char, the first is 0xe0
         */
        if (ichar == 0xe0)
        {
            ichar = getcmd_getch();
            switch(ichar)
            {
            case 'H':    /* up arrow */
                ichar = CTL_CH('p');
                break;
            case 'P':    /* down arrow */
                ichar = CTL_CH('n');
                break;
            case 'K':    /* left arrow */
                ichar = CTL_CH('b');
                break;
            case 'M':    /* right arrow */
                ichar = CTL_CH('f');
                break;
            case 'G':    /* Home */
                ichar = CTL_CH('a');
                break;
            case 'O':    /* End */
                ichar = CTL_CH('e');
                break;
            case 'S':    /* Delete */
                ichar = CTL_CH('d');
                break;
            default:
                break;
            }
        }

         /*
         * handle standard linux xterm esc sequences for arrow key, etc.
         */
        if (esc_len != 0)
        {
            if (esc_len == 1)
            {
                if (ichar == '[')
                {
                    esc_save[esc_len] = ichar;
                    esc_len = 2;
                }
                else
                {
                    cread_add_str(esc_save, esc_len, insert,
                              &num, &eol_num, buf, *len);
                    esc_len = 0;
                }
                continue;
            }

            switch (ichar)
            {
            case 'D':     /* <- key */
                ichar = CTL_CH('b');
                esc_len = 0;
                break;
            case 'C':     /* -> key */
                ichar = CTL_CH('f');
                esc_len = 0;
                break;     /* pass off to ^F handler */
            case 'H':     /* Home key */
                ichar = CTL_CH('a');
                esc_len = 0;
                break;     /* pass off to ^A handler */
            case 'A':     /* up arrow */
                ichar = CTL_CH('p');
                esc_len = 0;
                break;     /* pass off to ^P handler */
            case 'B':     /* down arrow */
                ichar = CTL_CH('n');
                esc_len = 0;
                break;     /* pass off to ^N handler */
            default:
                esc_save[esc_len++] = ichar;
                cread_add_str(esc_save, esc_len, insert,
                          &num, &eol_num, buf, *len);
                esc_len = 0;
                continue;
            }
        }
         /* End of linux arrow key */

        switch (ichar)
        {
         /* linux esc sequences */
        case 0x1b:
            if (esc_len == 0)
            {
                esc_save[esc_len] = ichar;
                esc_len = 1;
            }
            else
            {
                getcmd_puts("impossible condition #876\n");
                esc_len = 0;
            }
            break;

        case CTL_CH('a'):
            BEGINNING_OF_LINE();
            break;
        case CTL_CH('c'):     /* ^C - break */
            *buf = '\0';     /* discard input */
            return (-1);
        case CTL_CH('f'):
            if (num < eol_num)
            {
                getcmd_putch(buf[num]);
                num++;
            }
            break;
        case CTL_CH('b'):
            if (num)
            {
                getcmd_putch(CTL_BACKSPACE);
                num--;
            }
            break;
        case CTL_CH('d'):
            if (num < eol_num)
            {
                wlen = eol_num - num - 1;
                if (wlen)
                {
                    memmove(&buf[num], &buf[num+1], wlen);
                    putnstr(buf + num, wlen);
                }

                getcmd_putch(' ');
                do {
                    getcmd_putch(CTL_BACKSPACE);
                } while (wlen--);
                eol_num--;
            }
            break;
        case CTL_CH('k'):
            ERASE_TO_EOL();
            break;
        case CTL_CH('e'):
            REFRESH_TO_EOL();
            break;
        case CTL_CH('o'):
            insert = !insert;
            break;
        case CTL_CH('x'):
        case CTL_CH('u'):
            BEGINNING_OF_LINE();
            ERASE_TO_EOL();
            break;
        case DEL:
        case DEL7:
        case BACKSPACE:
            if (num)
            {
                wlen = eol_num - num;
                num--;
                memmove(&buf[num], &buf[num+1], wlen);
                getcmd_putch(CTL_BACKSPACE);
                putnstr(buf + num, wlen);
                getcmd_putch(' ');
                do {
                    getcmd_putch(CTL_BACKSPACE);
                } while (wlen--);
                eol_num--;
            }
            break;
        case CTL_CH('p'):
        case CTL_CH('n'):
        {
            char * hline;

            esc_len = 0;

            if (ichar == CTL_CH('p'))
                hline = hist_prev();
            else
                hline = hist_next();

            if (!hline)
            {
                getcmd_cbeep();
                continue;
            }

             /* nuke the current line */
             /* first, go home */
            BEGINNING_OF_LINE();

             /* erase to end of line */
            ERASE_TO_EOL();

             /* copy new line into place and display */
            strcpy(buf, hline);
            eol_num = (unsigned long)strlen(buf);
            REFRESH_TO_EOL();
            continue;
        }

        default:
            cread_add_char(ichar, insert, &num, &eol_num, buf, *len);
            break;
        }
    }

    *len = eol_num;
    buf[eol_num] = '\0';     /* lose the newline */

    if (buf[0] && buf[0] != CREAD_HIST_CHAR)
        cread_add_to_hist(buf);
    hist_cur = hist_add_idx;

    return 0;
}

完整处理代码请参考工程代码,或者参考uboot代码。
现在已经可以读取到了用户输入的字符串了,下一步,也是最后一步,就是执行这些命令(当然,不是支持的命令会提示出错信息)。

李迟 2020.9.30

©️2020 CSDN 皮肤主题: 技术工厂 设计师:CSDN官方博客 返回首页