1、介绍

easy_shell是由纯C语言编写,运行于嵌入式设备上的shell,通过串口作为命令传入,调用程序中的函数,相比于litter-shell削减了很多用不上的功能,本组件shell.c只有三百多行,简单易用好理解,目前支持且仅支持int类型、char类型、char *类型、hex类型的数据传入与类型自动识别,并且不用专门为shell写一个绑定函数。

2、实现过程

<1> 命令截取

嵌入式设备在接受串口数据时,检测有没有回车换行符,如果检测到则代表接收到了一条指令。由此我们不断接受串口传来的数据,当接收到'\n'或者'\r'时,将这个缓冲区的信息进行打包,并且为了方便后续的解析,需要在命令两头添加空格符:

uint8_t shell_handleFunc(const char c)
{
    static char input_string[SHELL_CMD_MAX_LEN];
    static uint8_t input_index = 1;
    if (('\n' == c) || ('\r' == c))
    {
        input_string[input_index] = ' ';
        input_string[0] = ' ';
        SHELL_Printf("=>:%s\r\n", input_string);
        cmd_analyze(input_string);
        memset(input_string, 0, SHELL_CMD_MAX_LEN);
        input_index = 1;
    }
    else
    {
        input_string[input_index++] = c;
        if(input_index>=SHELL_CMD_MAX_LEN)
        {
            input_index = 0;
            memset(input_string, 0, SHELL_CMD_MAX_LEN);
            SHELL_Printf("input to long\r\n");
        }
    }
}

同时我们要做一些异常处理如:最大长度判断,并且在每次解析完成一个指令后清空缓冲区,准备下一次处理

<2> 指令字符串以及传参字符串截取

我们先来分析上一步可能传入的命令截取到的字符串,假设用户输入cmd_test 1 A hello,代表我的指令是cmd_test,传入的参数为整形的'1',char类型的'A',与字符串类型的'hello',但是用户在实际使用过程中,写法可能没这么规矩,用户可能在输入过程中多按空格,不严格按照一个空格一个参数的方式来写,因此我们在解析时需要考虑这些情况,就有了上一步的在命令前后各增加一个空格,然后通过在空格转变为字符的地方,代表字符串开始,在字符转变为空格的地方,代表了字符串结束,我们通过遍历的方式将所有的指令和参数保存起来,后面备用
1.png

    while (c)
    {
        if (' ' != c && ' ' == last_c) //
        {
            cmd_item[item_num_index][item_str_index] = c;
            item_str_index++;
        }
        else if (' ' == c && ' ' != last_c)
        {
            item_num_index++;
            item_str_index = 0;
        }
        else
        {
            if (' ' != c)
            {
                cmd_item[item_num_index][item_str_index] = c;
                item_str_index++;
            }
        }

        index++;
        last_c = c;
        c = str[index];
    }

<3> 预测传入参数的类型

上一步中我们获得到了命令与传入参数的字符串,这些内容都是字符类型的,我们没办法直接使用,因此要猜测这大概是类型的数据,我总共定义了四种可能的类型

typedef enum para_type
{
    E_INT = 0x00,
    E_HEX = 0x01,
    E_CHAR = 0x02,
    E_STRING = 0x03,
} para_type_t;
  • 判断是不是INT类型,通过遍历这个字符串是不是数字字符就可以,当然,也要注意负号的判断
  • 如果不是INT类型,则通过判断参数的长度是否为1,是否是是字母类型来确认是不是CHAR类型
  • 如果不是char类型,通过判断字符串开头是不是'0x'或者是'0X'开头,并且后面的字符在0~9、a~f、A~F的范围中确认是不是HEX类型
  • 如果都不是上述的类型,那就是STRING类型,即char *

代码如下:

para_type_t cmd_get_para_type(const char *str)
{
    size_t len = strlen((char *)str);
    uint8_t index = 0;
    int i = 0;
    if('-' == str[0])
    {
        i = 1 ;
        index ++ ;
    }
    for (; i < len; i++)
    {
        if (str[i] >= '0' && str[i] <= '9')
        {
            index++;
        }
        else
        {
            break;
        }
    }
    if (index == len) // 字符串都在0~9 代表是数字
    {
        return E_INT;
    }
    else if (1 == len) // 长度为1
    {
        if ((str[0] >= 'A') && (str[0] <= 'Z') || (str[0] >= 'a') && (str[0] <= 'z')) // 长度为1 表示为char
        {
            return E_CHAR;
        }
    }
    else
    {
        if (('0' == str[0]) && (('x' == str[1]) || ('X' == str[1])))
        {
            uint8_t flag;
            for (i = 2; i < len; i++)
            {
                char a = str[i];
                if ((a >= 'A') && (a <= 'F') || (a >= 'a') && (a <= 'f') || (a >= '0') && (a <= '9'))
                {
                    flag++;
                }
            }
            if (len == flag + 2)
                return E_HEX;
        }
        return E_STRING;
    }
}

<4> 参数的保存

我们通过遍历的方式猜测了每种传入参数可能的类型,然后就需要解析将其保存下来,由于我设定的最长的参数数量为10个,因此我创建了一个uint64(x86_64)或者uint32(ARM_32)的数组来保存参数,用系统位数的变量既可以保存整形或者char,又可以保存指针类型

    #ifdef _WIN64
    uint64_t cmd_para[SHELL_CMD_MAX_PARA_NUM];
    #else
    uint32_t cmd_para[SHELL_CMD_MAX_PARA_NUM];
    #endif

根据猜测到的类型来解析值,保存在cmd_para数组中

    for (int i = 1; i < item_num_index; i++) //因为解析出来的第一个参数是指令本身,因此从1开始遍历
    {
        cmd_para_type[i] = cmd_get_para_type(cmd_item[i]);
        switch (cmd_para_type[i])
        {
        case E_INT:
            cmd_para[i] = cmd_item_analyze_int(cmd_item[i]);
            break;
        case E_CHAR:
            cmd_para[i] = cmd_item[i][0];
            break;
        case E_HEX:
            cmd_para[i] = cmd_item_analyze_hex(cmd_item[i]);
            break;
        case E_STRING:
            cmd_para[i] = cmd_item[i];
            break;
        default:
            break;
        }
    }

<5> 命令的查找与传参运行

用户可以使用shell_RegCmd("test1", test1);接口来注册命令,对于这种动态的数据,使用链表是极好的,在注册命令时,在链表后面挂节点接可以了,在查询命令时,遍历整个节点并对比命令字符串就OK了,链表相关的代码不在此处展示了。从链表中查询到相应的命令后,根据参数数量调用回调函数。

   ShellNode *shell_node = NULL;

    for (shell_node = ((ShellNode *)shellList.head.next); shell_node != ((ShellNode *)&shellList.tail); shell_node = ((ShellNode *)((Node *)shell_node)->next))
    {
        if (0 == strcmp(cmd_item[0], shell_node->name))
        {
            switch (item_num_index - 1) // 函数名也算是一个参数,因此在此减去一
            {
            case 1:
                shell_node->handleFunc(cmd_para[1]);
                break;
            case 2:
                shell_node->handleFunc(cmd_para[1], cmd_para[2]);
                break;
            case 3:
                shell_node->handleFunc(cmd_para[1], cmd_para[2], cmd_para[3]);
                break;
            case 4:
                shell_node->handleFunc(cmd_para[1], cmd_para[2], cmd_para[3],cmd_para[4]);
                break;
            case 5:
                shell_node->handleFunc(cmd_para[1], cmd_para[2], cmd_para[3],cmd_para[4], cmd_para[5]);
                break;
            case 6:
                shell_node->handleFunc(cmd_para[1], cmd_para[2], cmd_para[3],cmd_para[4], cmd_para[5], cmd_para[6]);
                break;
            case 7:
                shell_node->handleFunc(cmd_para[1], cmd_para[2], cmd_para[3],cmd_para[4], cmd_para[5], cmd_para[6],cmd_para[7]);
                break;
            case 8:
                shell_node->handleFunc(cmd_para[1], cmd_para[2], cmd_para[3],cmd_para[4], cmd_para[5], cmd_para[6],cmd_para[7], cmd_para[8]);
                break;
            case 9:
                shell_node->handleFunc(cmd_para[1], cmd_para[2], cmd_para[3],cmd_para[4], cmd_para[5], cmd_para[6],cmd_para[7], cmd_para[8], cmd_para[9]);
                break;
            default:
                break;
            }
            return;
        }
        else
        {
            continue;
        }
    }

3、编译与运行

代码已经同步上传至github,克隆到本地后在目录下使用gcc编译gcc *.c,结束后生成a.exe,运行后就可以进行命令交互了,运行输出:

PS C:\Users\13588\easy_shell> .\a.exe

shell Build: Jun 14 2023 18:38:17>>

测试函数test1

uint8_t test1(int a, char b, char *c)
{
    printf("a int :%d  a hex :0x%02x\r\n", a, a);
    printf("b:%c\r\n", b);
    printf("c:%s\r\n", c);
}
shell_RegCmd("test1", test1);

输入命令:test1 0xaf g hello
输出:

=>: test1 0xaf   g  hello 
a int :175  a hex :0xaf
b:g
c:hello

4、其他

代码是摸鱼时间偷偷写的,在函数变量命名方面非常不讲究,可能存在很多BUG,看到的大佬请不吝赐教,欢迎留言(/doge)

文章目录